Documentation updates
[pmount-gui.git] / main.c
1 /* Required for strdup, snprintf, getopt */
2 #define _XOPEN_SOURCE 500
3 #include <stdlib.h>
4 #include <stdio.h>
5 #include <string.h>
6 #include <unistd.h>
7 #include <fcntl.h>
8 #include <dirent.h>
9 #include <mntent.h>
10 #include <errno.h>
11 #include <sys/stat.h>
12 #include <sys/wait.h>
13 #include <sys/select.h>
14 #include <sys/inotify.h>
15 #include <gtk/gtk.h>
16 #include <gdk/gdkkeysyms.h>
17
18 typedef struct sProperty
19 {
20         char *name;
21         char *value;
22 } Property;
23
24 typedef struct sDevice
25 {
26         char *node;
27         char *devname;
28         char *label;
29         char *description;
30         char *mount_point;
31         time_t time;
32 } Device;
33
34 typedef struct sGuiContext
35 {
36         int manager;
37         int autohide;
38         Device *devices;
39         GtkWidget *window;
40         GtkWidget *list;
41         GtkWidget *button;
42         char *post_mount_command;
43         int inotify_fd;
44         int dev_wd;
45         GIOChannel *inotify_channel;
46         int refresh_pending;
47 } GuiContext;
48
49 int verbosity = 0;
50
51 /**
52 Parses a string of the form name=value and places the components in a Property
53 structure.  Returns 0 on success, or -1 if the string wasn't a valid property.
54 */
55 int parse_property(char *str, int size, Property *prop)
56 {
57         int equals = -1;
58         int i;
59
60         for(i=0; (equals<0 && i<size); ++i)
61                 if(str[i]=='=')
62                         equals = i;
63
64         if(equals<0)
65                 return -1;
66
67         prop->name = malloc(equals+1);
68         strncpy(prop->name, str, equals);
69         prop->name[equals] = 0;
70
71         prop->value = malloc(size-equals);
72         strncpy(prop->value, str+equals+1, size-equals-1);
73         prop->value[size-equals-1] = 0;
74
75         return 0;
76 }
77
78 /**
79 Retrieves all properties associated with a /dev node.  The returned array is
80 terminated with an entry containing NULL values.  Use free_properties to free
81 the array.
82 */
83 Property *get_device_properties(char *node)
84 {
85         int pid;
86         int pipe_fd[2];
87         char *buf;
88         int bufsize;
89         int pos = 0;
90         int eof = 0;
91         Property *props = NULL;
92         int n_props = 0;
93
94         pipe(pipe_fd);
95
96         pid = fork();
97         if(pid==0)
98         {
99                 /* Child process */
100                 if(verbosity>=2)
101                         printf("Running udevadm info -q property -n \"%s\"\n", node);
102
103                 close(pipe_fd[0]);
104                 dup2(pipe_fd[1], 1);
105
106                 execl("/sbin/udevadm", "udevadm", "info", "-q", "property", "-n", node, NULL);
107                 _exit(1);
108         }
109         else if(pid<0)
110         {
111                 close(pipe_fd[0]);
112                 close(pipe_fd[1]);
113
114                 return NULL;
115         }
116
117         /* Parent process */
118         close(pipe_fd[1]);
119
120         bufsize = 256;
121         buf = (char *)malloc(bufsize);
122
123         while(1)
124         {
125                 int newline;
126                 int i;
127                 Property prop;
128
129                 if(!eof)
130                 {
131                         int len;
132
133                         len = read(pipe_fd[0], buf+pos, bufsize-pos);
134                         if(len==0)
135                                 eof = 1;
136                         else if(len==-1)
137                                 break;
138                         pos += len;
139                 }
140
141                 newline = -1;
142                 for(i=0; (newline<0 && i<pos); ++i)
143                         if(buf[i]=='\n')
144                                 newline = i;
145
146                 if(newline<0)
147                 {
148                         if(eof)
149                                 break;
150
151                         /* There was no newline in the buffer but there is more output to
152                         be read.  Try again with a larger buffer. */
153                         bufsize *= 2;
154                         buf = (char *)realloc(buf, bufsize);
155                         continue;
156                 }
157
158                 if(parse_property(buf, newline, &prop)==0)
159                 {
160                         /* Reserve space for a sentinel value as well. */
161                         props = (Property *)realloc(props, (n_props+2)*sizeof(Property));
162                         props[n_props] = prop;
163                         ++n_props;
164
165                         memmove(buf, buf+newline+1, pos-newline-1);
166                         pos -= newline+1;
167                 }
168                 else
169                         break;
170         }
171
172         free(buf);
173
174         if(props)
175         {
176                 /* Terminate the array with NULL pointers. */
177                 props[n_props].name = NULL;
178                 props[n_props].value = NULL;
179         }
180
181         waitpid(pid, NULL, 0);
182         close(pipe_fd[0]);
183
184         return props;
185 }
186
187 /**
188 Looks for a property in an array of properties and returns its value.  Returns
189 NULL if the property was not found.
190 */
191 char *get_property_value(Property *props, char *name)
192 {
193         int i;
194         for(i=0; props[i].name; ++i)
195                 if(strcmp(props[i].name, name)==0)
196                         return props[i].value;
197         return NULL;
198 }
199
200 /**
201 Checks if a property has a specific value.  A NULL value is matched if the
202 property does not exist.
203 */
204 int match_property_value(Property *props, char *name, char *value)
205 {
206         char *v = get_property_value(props, name);
207         if(!v)
208                 return value==NULL;
209         return strcmp(v, value)==0;
210 }
211
212 /**
213 Frees an array of properties and all strings contained in it.
214 */
215 void free_properties(Property *props)
216 {
217         int i;
218         if(!props)
219                 return;
220         for(i=0; props[i].name; ++i)
221         {
222                 free(props[i].name);
223                 free(props[i].value);
224         }
225         free(props);
226 }
227
228 /**
229 Returns an array of user-mountable devices listed in fstab.
230 */
231 char **get_fstab_devices(void)
232 {
233         FILE *file;
234         struct mntent *me;
235         char **devices = NULL;
236         int n_devices = 0;
237
238         file = setmntent("/etc/fstab", "r");
239         if(!file)
240                 return NULL;
241
242         while((me = getmntent(file)))
243                 if(hasmntopt(me, "user")!=NULL)
244                 {
245                         devices = (char **)realloc(devices, (n_devices+2)*sizeof(char *));
246                         devices[n_devices] = strdup(me->mnt_fsname);
247                         ++n_devices;
248                 }
249
250         endmntent(file);
251         if(devices)
252                 devices[n_devices] = NULL;
253
254         return devices;
255 }
256
257 /**
258 Checks if an array of strings contains the specified string.
259 */
260 int is_in_array(char **array, char *str)
261 {
262         int i;
263         if(!array || !str)
264                 return 0;
265         for(i=0; array[i]; ++i)
266                 if(!strcmp(str, array[i]))
267                         return 1;
268         return 0;
269 }
270
271 /**
272 Frees an array of strings.
273 */
274 void free_string_array(char **array)
275 {
276         int i;
277         if(!array)
278                 return;
279         for(i=0; array[i]; ++i)
280                 free(array[i]);
281         free(array);
282 }
283
284 /**
285 Checks if a partition identified by a sysfs path is on a removable device.
286 */
287 int is_removable(char *devpath)
288 {
289         char fnbuf[256];
290         int len;
291         char *ptr;
292         int fd;
293
294         len = snprintf(fnbuf, sizeof(fnbuf), "/sys%s", devpath);
295         /* Default to not removable if the path was too long. */
296         if(len+10>=(int)sizeof(fnbuf))
297                 return 0;
298
299         /* We got a partition as a parameter, but the removable property is on the
300         disk.  Replace the last component with "removable". */
301         for(ptr=fnbuf+len; (ptr>fnbuf && *ptr!='/'); --ptr) ;
302         strcpy(ptr, "/removable");
303
304         fd = open(fnbuf, O_RDONLY);
305         if(fd!=-1)
306         {
307                 char c;
308                 read(fd, &c, 1);
309                 close(fd);
310                 if(c=='1')
311                 {
312                         if(verbosity>=2)
313                                 printf("  Removable\n");
314                         return 1;
315                 }
316                 if(verbosity>=2)
317                         printf("  Not removable\n");
318         }
319
320         return 0;
321 }
322
323 /**
324 Checks if a partition's disk or any of its parent devices are connected to any
325 of a set of buses.  The device is identified by a sysfs path.  The bus array
326 must be terminated with a NULL entry.
327 */
328 int check_buses(char *devpath, char **buses)
329 {
330         char fnbuf[256];
331         char *ptr;
332         int len;
333
334         len = snprintf(fnbuf, sizeof(fnbuf), "/sys%s", devpath);
335         /* Default to no match if the path was too long. */
336         if(len+10>=(int)sizeof(fnbuf))
337                 return 0;
338
339         for(ptr=fnbuf+len; ptr>fnbuf+12; --ptr)
340                 if(*ptr=='/')
341                 {
342                         char linkbuf[256];
343                         /* Replace the last component with "subsystem". */
344                         strcpy(ptr, "/subsystem");
345                         len = readlink(fnbuf, linkbuf, sizeof(linkbuf)-1);
346
347                         if(len!=-1)
348                         {
349                                 linkbuf[len] = 0;
350                                 /* Extract the last component of the subsystem symlink. */
351                                 for(; (len>0 && linkbuf[len-1]!='/'); --len) ;
352
353                                 if(verbosity>=2)
354                                 {
355                                         *ptr = 0;
356                                         printf("  Subsystem of %s is %s\n", fnbuf, linkbuf+len);
357                                 }
358
359                                 if(is_in_array(buses, linkbuf+len))
360                                         return 1;
361                         }
362                 }
363
364         return 0;
365 }
366
367 /**
368 Check if an array of properties describes a device that can be mounted.  An
369 array of explicitly allowed devices can be passed in as well.  Both arrays must
370 be terminated by a NULL entry.
371 */
372 int can_mount(Property *props, char **allowed)
373 {
374         static char *removable_buses[] = { "usb", "firewire", 0 };
375         char *devname;
376         char *devpath;
377         char *bus;
378
379         devname = get_property_value(props, "DEVNAME");
380         if(is_in_array(allowed, devname))
381                 return 1;
382
383         /* Special case for CD devices, since they are not partitions.  Only allow
384         mounting if media is inserted. */
385         if(match_property_value(props, "ID_TYPE", "cd") && match_property_value(props, "ID_CDROM_MEDIA", "1"))
386                 return 1;
387
388         /* Only allow mounting partitions. */
389         if(!match_property_value(props, "DEVTYPE", "partition"))
390                 return 0;
391
392         devpath = get_property_value(props, "DEVPATH");
393         if(is_removable(devpath))
394                 return 1;
395
396         /* Certain buses are removable by nature, but devices only advertise
397         themselves as removable if they support removable media, e.g. memory card
398         readers. */
399         bus = get_property_value(props, "ID_BUS");
400         if(is_in_array(removable_buses, bus))
401                 return 1;
402
403         return check_buses(devpath, removable_buses);
404 }
405
406 /**
407 Returns an array of all device nodes in a directory.  Symbolic links are
408 dereferenced.
409 */
410 char **get_device_nodes(char *dirname)
411 {
412         DIR *dir;
413         struct dirent *de;
414         char fnbuf[256];
415         char linkbuf[256];
416         struct stat st;
417         char **nodes = NULL;
418         int n_nodes = 0;
419         char **checked = NULL;
420         int n_checked = 0;
421         int i;
422
423         dir = opendir(dirname);
424         if(!dir)
425                 return NULL;
426
427         while((de = readdir(dir)))
428         {
429                 char *node;
430                 int duplicate = 0;
431
432                 /* Ignore . and .. entries. */
433                 if(de->d_name[0]=='.' && (de->d_name[1]==0 || (de->d_name[1]=='.' && de->d_name[2]==0)))
434                         continue;
435
436                 snprintf(fnbuf, sizeof(fnbuf), "%s/%s", dirname, de->d_name);
437
438                 node = fnbuf;
439                 lstat(fnbuf, &st);
440                 if(S_ISLNK(st.st_mode))
441                 {
442                         int len;
443                         len = readlink(fnbuf, linkbuf, sizeof(linkbuf)-1);
444                         if(len!=-1)
445                         {
446                                 linkbuf[len] = 0;
447                                 node = linkbuf;
448                         }
449                 }
450
451                 /* There may be multiple symlinks to the same device.  Only include each
452                 device once in the returned array. */
453                 if(checked)
454                 {
455                         for(i=0; (!duplicate && i<n_checked); ++i)
456                                 if(strcmp(node, checked[i])==0)
457                                         duplicate = 1;
458                 }
459                 if(duplicate)
460                 {
461                         if(verbosity>=2)
462                                 printf("Device %s is a duplicate\n", fnbuf);
463                         continue;
464                 }
465
466                 checked = (char **)realloc(checked, (n_checked+1)*sizeof(char *));
467                 checked[n_checked] = strdup(node);
468                 ++n_checked;
469
470                 nodes = (char **)realloc(nodes, (n_nodes+2)*sizeof(char *));
471                 nodes[n_nodes] = strdup(fnbuf);
472                 ++n_nodes;
473         }
474
475         closedir(dir);
476         if(checked)
477         {
478                 for(i=0; i<n_checked; ++i)
479                         free(checked[i]);
480                 free(checked);
481         }
482
483         if(nodes)
484                 nodes[n_nodes] = NULL;
485
486         return nodes;
487 }
488
489 /**
490 Reads the list of mounted devices from /etc/mtab and records the mount points.
491 */
492 void check_mounts(Device *devices)
493 {
494         FILE *file;
495         struct mntent *me;
496
497         file = setmntent("/etc/mtab", "r");
498         if(!file)
499                 return;
500
501         while((me = getmntent(file)))
502         {
503                 int i;
504
505                 for(i=0; devices[i].node; ++i)
506                         if(!strcmp(devices[i].devname, me->mnt_fsname))
507                         {
508                                 devices[i].mount_point = strdup(me->mnt_dir);
509
510                                 if(verbosity>=1)
511                                         printf("Device %s is mounted on %s\n", devices[i].node, devices[i].mount_point);
512                         }
513         }
514
515         endmntent(file);
516 }
517
518 /**
519 Returns an array of all mountable devices.
520 */
521 Device *get_devices(void)
522 {
523         char **nodes = NULL;
524         Device *devices = NULL;
525         int n_devices = 0;
526         char **mounted = NULL;
527         char **fstab = NULL;
528         int i;
529
530         nodes = get_device_nodes("/dev/disk/by-id");
531         fstab = get_fstab_devices();
532
533         for(i=0; nodes[i]; ++i)
534         {
535                 Property *props;
536
537                 if(verbosity>=1)
538                         printf("Examining device %s\n", nodes[i]);
539
540                 props = get_device_properties(nodes[i]);
541                 if(!props)
542                 {
543                         if(verbosity>=2)
544                                 printf("  No properties\n");
545                         continue;
546                 }
547
548                 if(verbosity>=2)
549                 {
550                         int j;
551                         for(j=0; props[j].name; ++j)
552                                 printf("  %s = %s\n", props[j].name, props[j].value);
553                 }
554
555                 if(can_mount(props, fstab))
556                 {
557                         char *devname;
558                         char *label;
559                         char *vendor;
560                         char *model;
561                         char buf[256];
562                         char pos;
563                         struct stat st;
564
565                         if(verbosity>=1)
566                                 printf("  Using device\n");
567
568                         devname = get_property_value(props, "DEVNAME");
569
570                         /* Get a human-readable label for the device.  Use filesystem label,
571                         filesystem UUID or device node name in order of preference. */
572                         label = get_property_value(props, "ID_FS_LABEL");
573                         if(!label)
574                                 label = get_property_value(props, "ID_FS_UUID");
575                         if(!label)
576                         {
577                                 char *ptr;
578
579                                 label = devname;
580                                 for(ptr=label; *ptr; ++ptr)
581                                         if(*ptr=='/')
582                                                 label = ptr+1;
583                         }
584
585                         vendor = get_property_value(props, "ID_VENDOR");
586                         model = get_property_value(props, "ID_MODEL");
587
588                         pos = snprintf(buf, sizeof(buf), "%s", label);
589                         if(vendor && model)
590                                 pos += snprintf(buf+pos, sizeof(buf)-pos, " (%s %s)", vendor, model);
591
592                         stat(nodes[i], &st);
593
594                         /* Reserve space for a sentinel entry. */
595                         devices = (Device *)realloc(devices, (n_devices+2)*sizeof(Device));
596                         devices[n_devices].node = nodes[i];
597                         devices[n_devices].devname = strdup(devname);
598                         devices[n_devices].label = strdup(label);
599                         devices[n_devices].description = strdup(buf);
600                         devices[n_devices].mount_point = NULL;
601                         devices[n_devices].time = st.st_mtime;
602                         ++n_devices;
603                 }
604                 else
605                         free(nodes[i]);
606                 free_properties(props);
607         }
608
609         free(nodes);
610         free_string_array(mounted);
611
612         if(devices)
613         {
614                 /* Terminate the array with NULL pointers. */
615                 devices[n_devices].node = NULL;
616                 devices[n_devices].devname = NULL;
617                 devices[n_devices].label = NULL;
618                 devices[n_devices].description = NULL;
619                 devices[n_devices].mount_point = NULL;
620
621                 check_mounts(devices);
622         }
623
624         return devices;
625 }
626
627 /**
628 Frees an array of devices and all strings contained in it.
629 */
630 void free_devices(Device *devices)
631 {
632         int i;
633         if(!devices)
634                 return;
635         for(i=0; devices[i].node; ++i)
636         {
637                 free(devices[i].node);
638                 free(devices[i].devname);
639                 free(devices[i].label);
640                 free(devices[i].description);
641                 if(devices[i].mount_point)
642                         free(devices[i].mount_point);
643         }
644         free(devices);
645 }
646
647 /**
648 Mounts a device if it was not mounted, or unmounts if it was.
649 */
650 int toggle_device(Device *device, char *out_buf, int out_size)
651 {
652         int umount = !!device->mount_point;
653         char mount_point[1024];
654         int pos = 0;
655         int status = 0;
656         fd_set fds;
657         struct timeval timeout;
658         int pid;
659         int pipe_fd[2];
660         char suffix = 0;
661
662         out_buf[0] = 0;
663
664         /* Find a mount point that does not exist yet. */
665         while(1)
666         {
667                 int len;
668
669                 len = snprintf(mount_point, sizeof(mount_point), "/media/%s", device->label);
670                 if(len+2>=(int)sizeof(mount_point))
671                         return -1;
672
673                 if(suffix)
674                         len += snprintf(mount_point+len, sizeof(mount_point)-len, "_%c", suffix);
675
676                 if(access(mount_point, F_OK)<0 && errno==ENOENT)
677                         break;
678
679                 if(suffix==0)
680                         suffix = '1';
681                 else if(suffix<'9')
682                         ++suffix;
683                 else
684                         return -1;
685         }
686
687         pipe(pipe_fd);
688
689         pid = fork();
690         if(pid==0)
691         {
692                 /* Child process */
693                 if(verbosity>=1)
694                 {
695                         if(umount)
696                                 printf("Running pumount %s\n", device->node);
697                         else
698                                 printf("Running pmount %s %s\n", device->node, mount_point+7);
699                 }
700
701                 close(pipe_fd[0]);
702                 dup2(pipe_fd[1], 1);
703                 dup2(pipe_fd[1], 2);
704
705                 if(umount)
706                         execl("/usr/bin/pumount", "pumount", device->node, NULL);
707                 else
708                         execl("/usr/bin/pmount", "pmount", device->node, mount_point+7, NULL);
709                 _exit(1);
710         }
711         else if(pid<0)
712                 return -1;
713
714         /* Parent process */
715
716         close(pipe_fd[1]);
717         FD_ZERO(&fds);
718         FD_SET(pipe_fd[0], &fds);
719         timeout.tv_sec = 0;
720         timeout.tv_usec = 200000;
721
722         while(1)
723         {
724                 /* The write fd for the pipe may be inherited by a fuse server
725                 process and stay open indefinitely. */
726                 if(select(pipe_fd[0]+1, &fds, NULL, NULL, &timeout))
727                 {
728                         int len;
729
730                         len = read(pipe_fd[0], out_buf+pos, out_size-pos-1);
731                         if(len<=0)
732                                 break;
733                         pos += len;
734                 }
735                 else if(waitpid(pid, &status, 0))
736                 {
737                         pid = 0;
738                         break;
739                 }
740         }
741
742         close(pipe_fd[0]);
743         if(pid)
744                 waitpid(pid, &status, 0);
745
746         out_buf[pos] = 0;
747
748         if(verbosity>=1)
749         {
750                 if(WIFEXITED(status))
751                 {
752                         if(WEXITSTATUS(status))
753                                 printf("Command exited successfully\n");
754                         else
755                                 printf("Command exited with status %d\n", WEXITSTATUS(status));
756                 }
757                 else if(WIFSIGNALED(status))
758                         printf("Command terminated with signal %d\n", WTERMSIG(status));
759                 else
760                         printf("Command exited with unknown result %04X\n", status);
761         }
762
763         if(!WIFEXITED(status) || WEXITSTATUS(status))
764                 return -1;
765
766         if(umount)
767         {
768                 free(device->mount_point);
769                 device->mount_point = NULL;
770         }
771         else
772                 device->mount_point = strdup(mount_point);
773
774         return 0;
775 }
776
777 /**
778 Refreshes both the internal devices array and the GUI list.
779 */
780 int refresh_devices(GuiContext *context, int umount)
781 {
782         GtkListStore *store;
783         GtkTreeSelection *selection;
784         int n_listed;
785         time_t latest;
786         int i;
787         GtkTreeIter iter;
788
789         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(context->list)));
790         gtk_list_store_clear(store);
791
792         free_devices(context->devices);
793
794         context->devices = get_devices();
795         if(!context->devices)
796                 return 0;
797
798         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(context->list));
799
800         /* Populate the list with devices in appropriate state. */
801         n_listed = 0;
802         latest = 0;
803         for(i=0; context->devices[i].node; ++i)
804         {
805                 Device *dev;
806
807                 dev = &context->devices[i];
808                 if(umount<0 || !dev->mount_point==!umount)
809                 {
810                         gtk_list_store_append(store, &iter);
811                         gtk_list_store_set(store, &iter,
812                                 0, dev->description,
813                                 1, dev,
814                                 2, !!dev->mount_point,
815                                 3, dev->mount_point,
816                                 -1);
817                         if(dev->time>latest)
818                         {
819                                 /* Pre-select the device that appeared on the system most recently. */
820                                 latest = dev->time;
821                                 gtk_tree_selection_select_iter(selection, &iter);
822                         }
823
824                         ++n_listed;
825                 }
826         }
827
828         return n_listed;
829 }
830
831 /**
832 Handles an automatic refresh of the device list in response to inotify events.
833 */
834 gboolean refresh_devices_idle(gpointer data)
835 {
836         GuiContext *context = (GuiContext *)data;
837         int n_listed;
838
839         n_listed = refresh_devices(context, -1);
840         context->refresh_pending = 0;
841
842         if(context->autohide)
843         {
844                 if(n_listed)
845                         gtk_widget_show_all(context->window);
846                 else
847                         gtk_widget_hide(context->window);
848         }
849
850         return FALSE;
851 }
852
853 /**
854 Callback for selection in the device list changing.  Updates the action button
855 label according to device status.
856 */
857 void selection_changed(GtkTreeSelection *selection, gpointer user_data)
858 {
859         GuiContext *context = (GuiContext *)user_data;
860         GtkTreeIter iter;
861         GtkTreeModel *model;
862         Device *device;
863
864         if(!gtk_tree_selection_get_selected(selection, &model, &iter))
865                 return;
866
867         gtk_tree_model_get(model, &iter, 1, &device, -1);
868         gtk_button_set_label(GTK_BUTTON(context->button), (device->mount_point ? "Unmount" : "Mount"));
869 }
870
871 /**
872 Callback for activating a row in the device list.  Mounts or unmounts the
873 device depending on operating mode.
874 */
875 void row_activated(GtkTreeView *list, GtkTreePath *path, GtkTreeViewColumn *column, gpointer user_data)
876 {
877         GuiContext *context = (GuiContext *)user_data;
878         GtkTreeModel *model;
879         GtkTreeIter iter;
880         Device *device;
881         int ret;
882         char output[1024];
883         int pid;
884
885         model = gtk_tree_view_get_model(list);
886
887         if(!gtk_tree_model_get_iter(model, &iter, path))
888                 return;
889
890         gtk_tree_model_get(model, &iter, 1, &device, -1);
891         ret = toggle_device(device, output, sizeof(output));
892         if(ret)
893         {
894                 GtkWidget *dialog;
895
896                 /* Pmount terminated with nonzero status or a signal.  Display an
897                 error to the user. */
898                 dialog = gtk_message_dialog_new(NULL, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "%s", output);
899                 if(context->manager)
900                         g_signal_connect(dialog, "response", G_CALLBACK(&gtk_widget_destroy), dialog);
901                 else
902                         g_signal_connect(dialog, "response", G_CALLBACK(&gtk_main_quit), NULL);
903                 gtk_widget_show_all(dialog);
904                 return;
905         }
906
907         if(context->manager)
908         {
909                 gtk_list_store_set(GTK_LIST_STORE(model), &iter,
910                         2, !!device->mount_point,
911                         3, device->mount_point,
912                         -1);
913
914                 gtk_button_set_label(GTK_BUTTON(context->button), (device->mount_point ? "Unmount" : "Mount"));
915         }
916         else
917                 gtk_main_quit();
918
919         if(context->post_mount_command && device->mount_point)
920         {
921                 if(verbosity>=1)
922                         printf("Running %s in %s\n", context->post_mount_command, device->mount_point);
923
924                 pid = fork();
925                 if(pid==0)
926                 {
927                         chdir(device->mount_point);
928                         execlp(context->post_mount_command, context->post_mount_command, NULL);
929                         _exit(1);
930                 }
931         }
932
933         (void)column;
934         (void)user_data;
935 }
936
937 /**
938 Callback for the mount/unmount button.  Causes the selected row in the device
939 list to be activated.
940 */
941 void button_clicked(GtkButton *button, gpointer user_data)
942 {
943         GuiContext *context = (GuiContext *)user_data;
944         GtkTreeSelection *selection;
945         GtkTreeIter iter;
946         GtkTreeModel *model;
947         GtkTreePath *path;
948         GtkTreeViewColumn *column;
949
950         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(context->list));
951         gtk_tree_selection_get_selected(selection, &model, &iter);
952         path = gtk_tree_model_get_path(model, &iter);
953         column = gtk_tree_view_get_column(GTK_TREE_VIEW(context->list), 0);
954         gtk_tree_view_row_activated(GTK_TREE_VIEW(context->list), path, column);
955         gtk_tree_path_free(path);
956
957         (void)button;
958 }
959
960 /**
961 Global key press callback for the window.
962 */
963 gboolean key_press(GtkWidget *widget, GdkEvent *event, gpointer user_data)
964 {
965         if(event->key.keyval==GDK_KEY_Escape)
966         {
967                 gtk_main_quit();
968                 return TRUE;
969         }
970
971         (void)widget;
972         (void)user_data;
973
974         return FALSE;
975 }
976
977 /**
978 Callback for inotify events.
979 */
980 gboolean inotify_event_available(GIOChannel *source, GIOCondition condition, gpointer user_data)
981 {
982         GuiContext *context = (GuiContext *)user_data;
983         int fd;
984         char eventbuf[sizeof(struct inotify_event)+NAME_MAX+1];
985         int len;
986
987         fd = g_io_channel_unix_get_fd(source);
988         len = read(fd, eventbuf, sizeof(eventbuf));
989         if(len>=(int)sizeof(struct inotify_event))
990         {
991                 if(!context->refresh_pending)
992                 {
993                         g_timeout_add(500, &refresh_devices_idle, context);
994                         context->refresh_pending = 1;
995                 }
996         }
997
998         (void)condition;
999
1000         return TRUE;
1001 }
1002
1003 void show_help(void)
1004 {
1005         printf("pmount-gui\n"
1006                 "Copyright (c) 2011-2015 Mikko Rasa, Mikkosoft Productions\n\n"
1007                 "Usage: pmount-gui [-v] [-u] [-r <command>] [-m|-M] [-h]\n\n"
1008                 "Options:\n"
1009                 "  -v  Increase verbosity\n"
1010                 "  -u  Unmount a device (default is mount)\n"
1011                 "  -r  Run a command after mounting\n"
1012                 "  -m  Start a persistent mount manager\n"
1013                 "  -M  Like -m, but hide the window if there are no devices\n"
1014                 "  -h  Display this help\n");
1015 }
1016
1017 int main(int argc, char **argv)
1018 {
1019         GuiContext context;
1020         GtkWidget *box;
1021         GtkWidget *viewport;
1022         GtkListStore *store;
1023         GtkTreeSelection *selection;
1024         GtkCellRenderer *mounted_toggle;
1025         int opt;
1026         int umount = 0;
1027         int n_listed;
1028
1029         context.manager = 0;
1030         context.autohide = 0;
1031         context.devices = NULL;
1032         context.post_mount_command = NULL;
1033         context.inotify_fd = -1;
1034         context.dev_wd = -1;
1035         context.inotify_channel = NULL;
1036         context.refresh_pending = 0;
1037
1038         gtk_init(&argc, &argv);
1039
1040         while((opt = getopt(argc, argv, "vur:mMh"))!=-1) switch(opt)
1041         {
1042         case 'v':
1043                 ++verbosity;
1044                 break;
1045         case 'u':
1046                 umount = 1;
1047                 break;
1048         case 'r':
1049                 context.post_mount_command = optarg;
1050                 break;
1051         case 'm':
1052                 context.manager = 1;
1053                 context.autohide = 0;
1054                 break;
1055         case 'M':
1056                 context.manager = 1;
1057                 context.autohide = 1;
1058                 break;
1059         case 'h':
1060                 show_help();
1061                 return 0;
1062         }
1063
1064         context.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
1065         gtk_container_set_border_width(GTK_CONTAINER(context.window), 5);
1066         g_signal_connect(context.window, "destroy", G_CALLBACK(&gtk_main_quit), NULL);
1067         g_signal_connect(context.window, "key-press-event", G_CALLBACK(&key_press), NULL);
1068
1069         box = gtk_vbox_new(FALSE, 5);
1070         gtk_container_add(GTK_CONTAINER(context.window), box);
1071
1072         viewport = gtk_viewport_new(NULL, NULL);
1073         gtk_viewport_set_shadow_type(GTK_VIEWPORT(viewport), GTK_SHADOW_IN);
1074         gtk_box_pack_start(GTK_BOX(box), viewport, TRUE, TRUE, 0);
1075
1076         context.list = gtk_tree_view_new();
1077         gtk_container_add(GTK_CONTAINER(viewport), context.list);
1078         g_signal_connect(context.list, "row-activated", G_CALLBACK(&row_activated), &context);
1079
1080         store = gtk_list_store_new(4, G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_BOOLEAN, G_TYPE_STRING);
1081         gtk_tree_view_set_model(GTK_TREE_VIEW(context.list), GTK_TREE_MODEL(store));
1082         gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(context.list),
1083                 -1, "Device", gtk_cell_renderer_text_new(), "text", 0, NULL);
1084
1085         if(context.manager)
1086         {
1087                 GtkTreeViewColumn *mounted_column;
1088                 GtkCellRenderer *mount_point_renderer;
1089
1090                 mounted_column = gtk_tree_view_column_new();
1091                 gtk_tree_view_column_set_title(mounted_column, "Mounted");
1092
1093                 mounted_toggle = gtk_cell_renderer_toggle_new();
1094                 gtk_tree_view_column_pack_start(mounted_column, mounted_toggle, FALSE);
1095                 gtk_tree_view_column_add_attribute(mounted_column, mounted_toggle, "active", 2);
1096
1097                 mount_point_renderer = gtk_cell_renderer_text_new();
1098                 gtk_tree_view_column_pack_start(mounted_column, mount_point_renderer, TRUE);
1099                 gtk_tree_view_column_add_attribute(mounted_column, mount_point_renderer, "text", 3);
1100
1101                 gtk_tree_view_insert_column(GTK_TREE_VIEW(context.list), mounted_column, -1);
1102
1103                 context.inotify_fd = inotify_init();
1104                 if(context.inotify_fd>=0)
1105                 {
1106                         context.dev_wd = inotify_add_watch(context.inotify_fd, "/dev/disk/by-id", IN_CREATE|IN_DELETE);
1107
1108                         context.inotify_channel = g_io_channel_unix_new(context.inotify_fd);
1109                         g_io_add_watch(context.inotify_channel, G_IO_IN, &inotify_event_available, &context);
1110                 }
1111                 else
1112                         printf("Warning: Unable to initialize inotify\n");
1113
1114                 umount = -1;
1115         }
1116
1117         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(context.list));
1118         g_signal_connect(selection, "changed", G_CALLBACK(&selection_changed), &context);
1119
1120         context.button = gtk_button_new_with_label(umount ? "Unmount" : "Mount");
1121         g_signal_connect(context.button, "clicked", G_CALLBACK(&button_clicked), &context);
1122         gtk_box_pack_start(GTK_BOX(box), context.button, FALSE, TRUE, 0);
1123
1124         n_listed = refresh_devices(&context, umount);
1125
1126         if(n_listed || (context.manager && !context.autohide))
1127                 gtk_widget_show_all(context.window);
1128         else if(!context.manager)
1129         {
1130                 GtkWidget *dialog;
1131
1132                 /* Don't show the main window if no devices were listed. */
1133                 dialog = gtk_message_dialog_new(NULL, 0, GTK_MESSAGE_WARNING, GTK_BUTTONS_OK,
1134                         "No devices to %s", (umount ? "unmount" : "mount"));
1135                 g_signal_connect(dialog, "response", G_CALLBACK(&gtk_main_quit), NULL);
1136                 gtk_widget_show_all(dialog);
1137         }
1138
1139         gtk_main();
1140
1141         free_devices(context.devices);
1142         if(context.inotify_fd>=0)
1143                 close(context.inotify_fd);
1144
1145         return 0;
1146 }