File Coverage

blib/lib/App/USBKeyCopyCon.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package App::USBKeyCopyCon;
2              
3 1     1   29248 use warnings;
  1         2  
  1         36  
4 1     1   5 use strict;
  1         3  
  1         59  
5              
6             =head1 NAME
7              
8             App::USBKeyCopyCon - GUI console for bulk copying of USB keys
9              
10             =cut
11              
12             our $VERSION = '1.02';
13              
14              
15             =head1 SYNOPSIS
16              
17             To launch the GUI application that this module implements, simply run the
18             supplied wrapper script:
19              
20             usb-key-copy-con
21              
22             =head1 DESCRIPTION
23              
24             This module implements an application for bulk copying USB flash drives
25             (storage devices). The application was developed to run on Linux and is
26             probably not particularly portable to other platforms.
27              
28             From a user's perspective the operation is simple:
29              
30             =over 4
31              
32             =item 1
33              
34             insert a 'master' USB key when prompted - the contents of the key will be
35             copied into a temporary directory on the hard drive, after which the key can be
36             removed
37              
38             =item 2
39              
40             insert blank keys into all available USB ports - the app will detect when each
41             new key is inserted, start the copy process and alert the user on completion
42              
43             =item 3
44              
45             repeat step 2 as required
46              
47             =back
48              
49             The program can write to multiple keys in parallel. It can also use filtering
50             on device parameters to only overwrite devices which match the vendor name
51             and storage capacity specified - other devices will be ignored.
52              
53             The specifics of reading the master key, preparing a blank key (formatting
54             parameters etc) are implemented in short 'profile' scripts (a reader and a
55             writer). You can supply your own profile scripts if your requirements differ
56             from those provided.
57              
58             =head1 DEVELOPER INFORMATION
59              
60             The remainder of the documentation is targetted at developers who wish to
61             modify or customise the application.
62              
63             The application uses the Gtk2 GUI toolkit. The wrapper script instantiates a
64             single application object like this:
65              
66             use App::USBKeyCopyCon;
67              
68             App::USBKeyCopyCon->new->run;
69              
70             The constructor is responsible for building the user interface and the C<run>
71             method invokes the Gtk2 event loop. UI events are dispatched as method calls
72             on the application object.
73              
74             =cut
75              
76 1     1   492 use Moose;
  0            
  0            
77              
78             use Gtk2 -init;
79             use Glib qw(TRUE FALSE);
80             use Gtk2::SimpleMenu;
81              
82             use App::USBKeyCopyCon::Chrome;
83              
84             use Net::DBus;
85             use Net::DBus::GLib;
86             use Net::DBus::Dumper;
87              
88             use POSIX qw(:sys_wait_h);
89             use IO::Handle qw();
90             use File::Path qw(mkpath rmtree);
91             use File::Spec qw();
92              
93             use Data::Dumper;
94              
95             has 'current_state' => ( is => 'rw', isa => 'Str', default => '' );
96             has 'sudo_path' => ( is => 'rw', isa => 'Str', default => '' );
97             has 'master_info' => ( is => 'rw' );
98             has 'options' => ( is => 'rw', default => sub { {} } );
99             has 'profiles' => ( is => 'rw', default => sub { {} } );
100             has 'selected_profile' => ( is => 'rw', isa => 'Str', default => '' );
101             has 'automount_state' => ( is => 'rw', isa => 'Str', default => undef );
102             has 'temp_root' => ( is => 'rw', isa => 'Str', default => undef );
103             has 'master_root' => ( is => 'rw', isa => 'Str', default => undef );
104             has 'mount_dir' => ( is => 'rw', isa => 'Str', default => undef );
105             has 'volume_label' => ( is => 'rw', isa => 'Str', default => '' );
106             has 'selected_sound' => ( is => 'rw', isa => 'Str', default => '' );
107             has 'current_keys' => ( is => 'ro', default => sub { {} } );
108             has 'exit_status' => ( is => 'ro', default => sub { {} } );
109             has 'app_win' => ( is => 'rw', isa => 'Gtk2::Window' );
110             has 'key_rack' => ( is => 'rw', isa => 'Gtk2::Container' );
111             has 'console' => ( is => 'rw', isa => 'Gtk2::TextView' );
112             has 'vendor_combo' => ( is => 'rw', isa => 'Gtk2::ComboBox' );
113             has 'vendor_entry' => ( is => 'rw', isa => 'Gtk2::Entry' );
114             has 'capacity_combo' => ( is => 'rw', isa => 'Gtk2::ComboBox' );
115             has 'capacity_entry' => ( is => 'rw', isa => 'Gtk2::Entry' );
116             has 'hal' => ( is => 'rw', isa => 'Net::DBus::RemoteObject' );
117              
118              
119              
120             my @menu_entries = (
121             # name, stock id, label
122             [ "FileMenu", undef, "_File" ],
123             [ "EditMenu", undef, "_Edit" ],
124             [ "HelpMenu", undef, "_Help" ],
125             # name, stock id, label, accelerator, tooltip, action
126             [ "New", 'gtk-new', "_New master key", "<control>N", "Re-read the master key", 'file_new' ],
127             [ "Quit", 'gtk-quit', "_Quit", "<control>Q", "Quit", 'file_quit' ],
128             [ "Prefs", 'gtk-preferences', "_Preferences", "<control>E", "About", 'edit_preferences' ],
129             [ "About", 'gtk-about', "_About", "<control>A", "About", 'help_about' ],
130             );
131              
132             my $menu_ui = "<ui>
133             <menubar name='MenuBar'>
134             <menu action='FileMenu'>
135             <menuitem action='New'/>
136             <menuitem action='Quit'/>
137             </menu>
138             <menu action='EditMenu'>
139             <menuitem action='Prefs'/>
140             </menu>
141             <menu action='HelpMenu'>
142             <menuitem action='About'/>
143             </menu>
144             </menubar>
145             </ui>";
146              
147             my %hal_key_map = (
148             'info.udi' => 'udi',
149             'info.vendor' => 'vendor',
150             'info.product' => 'product',
151             'block.device' => 'block_device',
152             'storage.removable.media_size' => 'media_size',
153             'linux.sysfs_path' => 'sysfs_path',
154             );
155              
156             my $gconf_automount_path = '/apps/nautilus/preferences/media_automount';
157              
158             use constant VENDOR_EXACT => 0;
159             use constant VENDOR_PATTERN => 1;
160             use constant VENDOR_ANY => 2;
161             use constant CAPACITY_EXACT => 0;
162             use constant CAPACITY_MINIMUM => 1;
163             use constant CAPACITY_ANY => 2;
164              
165              
166             sub BUILD {
167             my $self = shift;
168              
169             $self->check_for_root_user;
170             $self->set_temp_root('/tmp');
171             $self->scan_for_profiles;
172             $self->select_profile;
173             $self->disable_automount;
174              
175             my($path) = __FILE__ =~ m{^(.*)[.]pm$};
176             $path = File::Spec->rel2abs($path) . "/copy-complete.wav";
177             $self->selected_sound($path);
178              
179             $self->build_ui;
180              
181             $self->init_dbus_watcher;
182              
183             $self->require_master_key;
184             }
185              
186              
187             sub sudo_wrap {
188             my($self, $command, @env_vars) = @_;
189              
190             my $sudo = $self->sudo_path or return $command;
191              
192             if($sudo =~ /gksudo/) {
193             my $msg = "The application 'usb-key-copy-con' requires administrative "
194             . "privileges to access USB flash drives";
195             return qq{$sudo --preserve-env --message "$msg" "$command"};
196             }
197              
198             my $env = join '', map { qq($_="$ENV{$_}" ) } @env_vars;
199             return qq{$sudo $env $command}
200             }
201              
202              
203             sub find_command {
204             my($self, $command) = @_;
205              
206             foreach my $dir (split /:/, $ENV{PATH}) {
207             my $path = "$dir/$command";
208             return $path if -x $path;
209             }
210             return;
211             }
212              
213              
214             sub commandline_options {
215             my $class = shift;
216             return(
217             'help|?',
218             '--no-root-check|n',
219             '--profile|p=s',
220             '--profile-dir|d=s'
221             );
222             }
223              
224              
225             sub scan_for_profiles {
226             my $self = shift;
227              
228             my($path) = File::Spec->rel2abs(__FILE__) =~ m{^(.*)[.]pm$};
229             my @profile_dirs = ($path . "/profiles");
230              
231             if(my $custom = $self->options->{'profile-dir'}) {
232             push @profile_dirs, File::Spec->rel2abs($custom);
233             }
234              
235             my $result = {};
236             foreach my $dir (@profile_dirs) {
237             foreach my $script (glob("$dir/*")) {
238             my($profile, $mode) = $script =~ m{^.*/([^/]+)-(reader|writer)[.]\w+$}
239             or next;
240             $result->{$profile}->{$mode} = $script;
241             }
242             }
243             die "Unable to locate any profile scripts" if not keys %$result;
244              
245             $self->profiles($result);
246             }
247              
248              
249             sub select_profile {
250             my($self, $profile) = @_;
251              
252             $profile ||= $self->options->{profile} || 'copyfiles';
253             if(not $self->profiles->{$profile}) {
254             die "Invalid profile name: '$profile'\n"
255             . "Known profiles: "
256             . join(', ', keys %{$self->profiles})
257             . "\n";
258             }
259             $self->selected_profile($profile);
260             my($path) = __FILE__ =~ m{^(.*)[.]pm$};
261             }
262              
263             sub reader_script {
264             my($self) = @_;
265             my $profile = $self->profiles->{$self->selected_profile} or return;
266             return $profile->{reader};
267             }
268              
269             sub writer_script {
270             my($self) = @_;
271             my $profile = $self->profiles->{$self->selected_profile} or return;
272             return $profile->{writer};
273             }
274              
275              
276             sub check_for_root_user {
277             my $self = shift;
278              
279             return if $self->options->{'no-root-check'};
280              
281             return if $> == 0;
282              
283             my $path = $self->find_command('gksudo') || $self->find_command('sudo');
284             if($path) {
285             $self->sudo_path($path);
286             return;
287             }
288              
289             die "You must either run this program as root or install sudo\n";
290             }
291              
292              
293             sub disable_automount {
294             my $self = shift;
295              
296             my $state = `gconftool-2 --get $gconf_automount_path 2>/dev/null`;
297             return if !defined($state) or $? != 0;
298              
299             chomp($state);
300             $self->automount_state($state);
301             system("gconftool-2 --type bool --set $gconf_automount_path false 2>/dev/null");
302             }
303              
304              
305             sub restore_automount {
306             my $self = shift;
307              
308             my $state = $self->automount_state or return;
309             system("gconftool-2 --type bool --set $gconf_automount_path $state 2>/dev/null");
310              
311             }
312              
313              
314             sub build_ui {
315             my $self = shift;
316              
317             my $window = Gtk2::Window->new;
318             $self->app_win($window);
319             $window->signal_connect(destroy => sub { Gtk2->main_quit; });
320             $window->set_title('USB Key Copying Console');
321             $window->set_default_size(850, 250);
322              
323             my $vbox = Gtk2::VBox->new(FALSE);
324             $vbox->pack_start($self->build_menu, FALSE, FALSE, 0);
325             $vbox->pack_start($self->build_filters, FALSE, FALSE, 0);
326             $vbox->pack_start(Gtk2::HSeparator->new, FALSE, TRUE, 0);
327             $vbox->pack_start($self->build_key_rack, FALSE, FALSE, 0);
328             $vbox->pack_start($self->build_console, TRUE, TRUE, 0);
329             $window->add($vbox);
330              
331             $window->show_all;
332             }
333              
334              
335             sub init_dbus_watcher {
336             my $self = shift;
337              
338             my $bus = Net::DBus::GLib->system;
339              
340             my $hal = $bus->get_service("org.freedesktop.Hal");
341              
342             my $manager = $hal->get_object(
343             "/org/freedesktop/Hal/Manager", "org.freedesktop.Hal.Manager"
344             );
345             $self->hal($manager);
346              
347             $manager->connect_to_signal('DeviceAdded', sub {
348             $self->hal_device_added(@_);
349             });
350              
351             $manager->connect_to_signal('DeviceRemoved', sub {
352             $self->hal_device_removed(@_);
353             });
354             }
355              
356              
357             sub require_master_key {
358             my $self = shift;
359              
360             if(not $self->reader_script) {
361             return $self->ready_to_write;
362             }
363             $self->current_state('MASTER-WAIT');
364             $self->disable_filter_inputs;
365             $self->say("Waiting for USB master key ...\n");
366             }
367              
368              
369             sub hal_device_added {
370             my($self, $target_udi) = @_;
371              
372             return unless $target_udi =~ /storage/;
373             my $prop = $self->hal_device_properties($target_udi) or return;
374              
375             if($self->current_state eq 'MASTER-WAIT') {
376             $self->start_master_read($prop);
377             return;
378             }
379             elsif($self->current_state eq 'COPYING') {
380             if($self->match_device_filter($prop)) {
381             $self->say("Device added: $prop->{block_device}\n");
382             }
383             else {
384             $self->say(" - device ignored\n");
385             $prop->{ignored} = 1;
386             }
387             #$self->say(Dumper($prop));
388             $self->add_key_to_rack($prop);
389             }
390             }
391              
392              
393             sub hal_device_removed {
394             my($self, $target_udi) = @_;
395              
396             return unless $target_udi =~ /storage/;
397              
398             my $state = $self->current_state;
399             if($state eq 'MASTER-COPIED') {
400             if($self->master_info->{udi} eq $target_udi) {
401             $self->ready_to_write;
402             }
403             }
404             elsif($state eq 'COPYING') {
405             if(my $dev = $self->current_keys->{$target_udi}) {
406             $self->say("Device removed: $dev->{block_device}\n");
407             }
408             $self->remove_key_from_rack($target_udi);
409             }
410             }
411              
412              
413             sub ready_to_write {
414             my($self) = @_;
415              
416             $self->say("Insert blank keys - copying will start automatically\n");
417             $self->enable_filter_inputs;
418             $self->current_state('COPYING');
419             }
420              
421              
422             sub hal_device_properties {
423             my($self, $target_udi) = @_;
424              
425             foreach my $dev ( @{ $self->hal->GetAllDevicesWithProperties } ) {
426             my($udi, $prop) = @$dev;
427             if($udi eq $target_udi) {
428             my $info = {};
429             while(my($hal_key, $key) = each %hal_key_map) {
430             $info->{$key} = $prop->{$hal_key} or return;
431             }
432             ($info->{dev}) = $info->{block_device} =~ m{/([^/]+)$};
433             #$self->say(Dumper($prop));
434             return $info;
435             }
436             }
437             }
438              
439              
440             sub match_device_filter {
441             my($self, $key_info) = @_;
442              
443             my $vendor_type = $self->vendor_combo->get_active;
444             my $vendor_text = $self->vendor_entry->get_text;
445             my $capacity_type = $self->capacity_combo->get_active;
446             my $capacity_text = $self->capacity_entry->get_text;
447              
448             if($vendor_type != VENDOR_ANY) {
449             if($vendor_text eq '') {
450             $self->say("Vendor filter not set");
451             return FALSE;
452             }
453             elsif($vendor_type == VENDOR_EXACT) {
454             if($key_info->{vendor} ne $vendor_text) {
455             $self->say("Vendor '$key_info->{vendor}' does not match");
456             return FALSE;
457             }
458             }
459             elsif($vendor_type == VENDOR_PATTERN) {
460             if($key_info->{vendor} !~ /$vendor_text/) {
461             $self->say("Vendor '$key_info->{vendor}' does not match");
462             return FALSE;
463             }
464             }
465             }
466              
467             if($capacity_type != CAPACITY_ANY) {
468             if($capacity_text eq '') {
469             $self->say("Capacity filter not set");
470             return FALSE;
471             }
472             elsif($capacity_type == CAPACITY_EXACT) {
473             if($key_info->{media_size} != $capacity_text) {
474             $self->say("Capacity '$key_info->{media_size}' does not match");
475             return FALSE;
476             }
477             }
478             elsif($capacity_type == CAPACITY_MINIMUM) {
479             if($key_info->{media_size} < $capacity_text) {
480             $self->say("Capacity '$key_info->{media_size}' too small");
481             return FALSE;
482             }
483             }
484             }
485              
486             return TRUE;
487             }
488              
489              
490             sub start_master_read {
491             my($self, $key_info) = @_;
492              
493             $self->confirm_master_dialog($key_info) or return;
494             $self->master_info($key_info);
495             $self->vendor_combo->set_active(VENDOR_EXACT);
496             $self->vendor_entry->set_text($key_info->{vendor});
497             $self->capacity_combo->set_active(CAPACITY_EXACT);
498             $self->capacity_entry->set_text($key_info->{media_size});
499              
500             $self->say("Reading master key\n");
501              
502             pipe(my $rd, my $wr) or die "pipe(): $!";
503             my $pid = fork();
504             if($pid == 0) { # In the child
505             sleep(2);
506             $ENV{USB_BLOCK_DEVICE} = $key_info->{block_device};
507             $ENV{USB_MOUNT_DIR} = $self->mount_dir . "/$key_info->{dev}";
508             $ENV{USB_MASTER_ROOT} = $self->master_root;
509             mkpath($ENV{USB_MOUNT_DIR}) if not -d $ENV{USB_MOUNT_DIR};
510             close($rd);
511             close STDOUT;
512             open STDOUT, '>&', $wr or die "error reopening STDOUT: $!";
513             close STDERR;
514             open STDERR, '>&', $wr or die "error reopening STDERR: $!";
515             my $command = $self->sudo_wrap(
516             $self->reader_script,
517             qw(USB_BLOCK_DEVICE USB_MOUNT_DIR USB_MASTER_ROOT),
518             );
519             exec($command) or die "Error starting reader script: $!";
520             exit; # never reached;
521             }
522             close($wr);
523             $rd->blocking(0);
524             Glib::IO->add_watch(
525             fileno($rd), ['in', 'err', 'hup'],
526             sub { $self->on_master_pipe_read(@_); },
527             );
528             $key_info->{pid} = $pid;
529             $key_info->{fh} = $rd;
530              
531             $self->current_state('MASTER-COPYING');
532             }
533              
534              
535             sub on_master_pipe_read {
536             my($self, $fd, $cond) = @_;
537              
538             my $key_info = $self->master_info or return FALSE;
539             my $fh = $key_info->{fh};
540             my $buffer;
541             if(sysread($fh, $buffer, 100000)) {
542             $self->say($buffer);
543             return TRUE;
544             }
545             close($fh);
546             delete $key_info->{fh};
547             return FALSE;
548             }
549              
550              
551             sub add_key_to_rack {
552             my($self, $key_info) = @_;
553              
554             my $key_widget = Gtk2::VBox->new(FALSE, 0);
555             my $pixbuf = $key_info->{ignored}
556             ? App::USBKeyCopyCon::Chrome::usb_icon('ignored')
557             : App::USBKeyCopyCon::Chrome::usb_icon(0);
558             my $icon = Gtk2::Image->new_from_pixbuf($pixbuf);
559             $key_widget->pack_start($icon, FALSE, FALSE, 2);
560             my $label = Gtk2::Label->new($key_info->{dev});
561             $key_widget->pack_start($label, FALSE, FALSE, 2);
562              
563             $self->key_rack->pack_start($key_widget, FALSE, FALSE, 0);
564             $key_widget->show_all;
565             $key_info->{widget} = $key_widget;
566             $key_info->{icon_widget} = $icon;
567              
568             $self->current_keys->{ $key_info->{udi} } = $key_info;
569              
570             if(not $key_info->{ignored}) {
571             $self->fork_copier($key_info);
572             }
573             }
574              
575              
576             sub fork_copier {
577             my($self, $key_info) = @_;
578              
579             pipe(my $rd, my $wr) or die "pipe(): $!";
580             my $pid = fork();
581             if($pid == 0) { # In the child
582             sleep(2);
583             $ENV{USB_BLOCK_DEVICE} = $key_info->{block_device};
584             $ENV{USB_MOUNT_DIR} = $self->mount_dir . "/$key_info->{dev}";
585             $ENV{USB_MASTER_ROOT} = $self->master_root;
586             $ENV{USB_VOLUME_NAME} = $self->volume_label;
587             mkpath($ENV{USB_MOUNT_DIR}) if not -d $ENV{USB_MOUNT_DIR};
588             close($rd);
589             close STDOUT;
590             open STDOUT, '>&', $wr or die "error reopening STDOUT: $!";
591             close STDERR;
592             open STDERR, '>&', $wr or die "error reopening STDERR: $!";
593             my $command = $self->sudo_wrap(
594             $self->writer_script,
595             qw(USB_BLOCK_DEVICE USB_MOUNT_DIR USB_MASTER_ROOT USB_VOLUME_NAME),
596             );
597             exec($command) or die "Error starting copy script: $!";
598             exit; # never reached;
599             }
600             close($wr);
601             $rd->blocking(0);
602             Glib::IO->add_watch(
603             fileno($rd), ['in', 'err', 'hup'],
604             sub { $self->on_copier_pipe_read(@_); },
605             $key_info->{udi}
606             );
607             $key_info->{pid} = $pid;
608             $key_info->{fh} = $rd;
609             $key_info->{output} = '';
610             $key_info->{status} = 0;
611             }
612              
613              
614             sub on_copier_pipe_read {
615             my($self, $fd, $cond, $udi) = @_;
616              
617             my $key_info = $self->current_keys->{$udi} or return FALSE;
618             my $fh = $key_info->{fh};
619             my $buffer;
620             if(sysread($fh, $buffer, 100000)) {
621             $key_info->{output} .= $buffer;
622             if($key_info->{output} =~ m/\A.*^\{(\d+)\/(\d+)\}/sm) {
623             $self->update_key_progress($udi, int(9 * $1 / $2));
624             }
625             return TRUE;
626             }
627             close($fh);
628             delete $key_info->{fh};
629             return FALSE;
630             }
631              
632              
633             sub remove_key_from_rack {
634             my($self, $udi) = @_;
635              
636             my $key_info = delete $self->current_keys->{$udi} or return;
637             $self->key_rack->remove($key_info->{widget});
638             return;
639             }
640              
641              
642             sub update_key_progress {
643             my($self, $udi, $status) = @_;
644              
645             $status = -1 if !defined $status or $status < -1 or $status > 10;
646              
647             my $key_info = $self->current_keys->{$udi} or return;
648             $key_info->{status} = $status;
649             $key_info->{icon_widget}->set_from_pixbuf(
650             App::USBKeyCopyCon::Chrome::usb_icon($status)
651             );
652             }
653              
654              
655             sub on_menu_file_new {
656             my $self = shift;
657             $self->require_master_key;
658             }
659              
660              
661             sub on_menu_file_quit {
662             my $self = shift;
663             # TODO: check for work in progress
664             # TODO: check if desktop automount should be re-enabled
665             Gtk2->main_quit;
666             }
667              
668              
669             sub on_menu_edit_preferences {
670             my $self = shift;
671             $self->say("Edit>Preferences - not implemented\n");
672             }
673              
674              
675             sub on_menu_help_about {
676             my $self = shift;
677              
678             my $dialog = Gtk2::Dialog->new(
679             'About: usb-key-copy-con',
680             $self->app_win,
681             [qw/modal destroy-with-parent/],
682             'gtk-close' => 'ok',
683             );
684             $dialog->set_default_size (90, 80);
685              
686             my $panel = Gtk2::VBox->new(FALSE, 12);
687              
688             my $title = Gtk2::Label->new;
689             $title->set_markup("<span font_desc='sans 20'> USB Key Copy Console </span>");
690             $title->set_selectable(TRUE);
691             $panel->pack_start($title, FALSE, FALSE, 10);
692              
693             my $version = Gtk2::Label->new;
694             $version->set_markup("<span font_desc='sans 16'>Version: $VERSION</span>");
695             $version->set_selectable(TRUE);
696             $panel->pack_start($version, FALSE, FALSE, 0);
697              
698             my $author = Gtk2::Label->new;
699             my $detail = '(c) 2009 Grant McLean &lt;grantm@cpan.org&gt;';
700             $author->set_markup(" <span font_desc='sans 10'>$detail</span> ");
701             $author->set_selectable(TRUE);
702             $panel->pack_start($author, FALSE, FALSE, 10);
703              
704             $dialog->vbox->pack_start($panel, FALSE, FALSE, 4);
705             $dialog->show_all;
706              
707             $dialog->run;
708              
709             $dialog->destroy;
710             }
711              
712              
713             sub build_menu {
714             my $self = shift;
715              
716             foreach my $item (@menu_entries) {
717             if(exists $item->[5]) {
718             my $action = 'on_menu_' . $item->[5];
719             $item->[5] = sub { $self->$action(@_) };
720             }
721             }
722             my $actions = Gtk2::ActionGroup->new("Actions");
723             $actions->add_actions(\@menu_entries, undef);
724              
725             my $ui = Gtk2::UIManager->new;
726             $ui->insert_action_group($actions, 0);
727             $self->app_win->add_accel_group($ui->get_accel_group);
728              
729             $ui->add_ui_from_string ($menu_ui);
730              
731             return $ui->get_widget('/MenuBar');
732             }
733              
734              
735             sub build_key_rack {
736             my $self = shift;
737              
738             my $box = Gtk2::HBox->new(FALSE, 4);
739              
740             $self->key_rack($box);
741              
742             return $box;
743             }
744              
745              
746             sub build_filters {
747             my $self = shift;
748              
749             my $box = Gtk2::HBox->new(FALSE, 4);
750              
751             my $label = Gtk2::Label->new("Filter parameters:");
752             $box->pack_start($label, FALSE, FALSE, 10);
753              
754             my $vendor_combo = Gtk2::ComboBox->new_text;
755             $vendor_combo->append_text('Exactly match vendor');
756             $vendor_combo->append_text('Pattern match vendor');
757             $vendor_combo->append_text('Match any vendor');
758             $vendor_combo->set_active(VENDOR_EXACT);
759             #$vendor_combo->signal_connect(changed => sub { $self->apply_filter(); });
760             $box->pack_start($vendor_combo, FALSE, FALSE, 10);
761             $self->vendor_combo($vendor_combo);
762              
763             my $vendor_entry = Gtk2::Entry->new;
764             $vendor_entry->set_width_chars(11);
765             $vendor_entry->set_text('');
766             $box->pack_start($vendor_entry, FALSE, FALSE, 0);
767             $self->vendor_entry($vendor_entry);
768              
769             my $capacity_combo = Gtk2::ComboBox->new_text;
770             $capacity_combo->append_text('Exactly match capacity');
771             $capacity_combo->append_text('Match minimum capacity');
772             $capacity_combo->append_text('Match any capacity');
773             $capacity_combo->set_active(CAPACITY_EXACT);
774             #$capacity_combo->signal_connect(changed => sub { $self->apply_filter(); });
775             $box->pack_start($capacity_combo, FALSE, FALSE, 10);
776             $self->capacity_combo($capacity_combo);
777              
778             my $capacity_entry = Gtk2::Entry->new;
779             $capacity_entry->set_width_chars(11);
780             $capacity_entry->set_text('');
781             $box->pack_start($capacity_entry, FALSE, FALSE, 0);
782             $self->capacity_entry($capacity_entry);
783              
784             return $box;
785             }
786              
787              
788             sub disable_filter_inputs { shift->_set_filter_sensitive(FALSE) }
789             sub enable_filter_inputs { shift->_set_filter_sensitive(TRUE) }
790              
791              
792             sub _set_filter_sensitive {
793             my($self, $state) = @_;
794              
795             $self->vendor_combo->set_sensitive($state);
796             $self->vendor_entry->set_sensitive($state);
797             $self->capacity_combo->set_sensitive($state);
798             $self->capacity_entry->set_sensitive($state);
799             }
800              
801              
802             sub build_console {
803             my $self = shift;
804              
805             my $scrolled_window = Gtk2::ScrolledWindow->new;
806             $scrolled_window->set_policy('automatic', 'automatic');
807             $scrolled_window->set_shadow_type('in');
808              
809             my $buffer = Gtk2::TextBuffer->new(undef);
810             $buffer->delete($buffer->get_bounds);
811              
812             my $console = Gtk2::TextView->new_with_buffer($buffer);
813             $console->set_editable(FALSE);
814             $console->set_cursor_visible(FALSE);
815             $console->set_wrap_mode('char');
816              
817             my $end_mark = $buffer->create_mark( 'end', $buffer->get_end_iter, FALSE);
818             $buffer->signal_connect(
819             insert_text => sub {
820             $console->scroll_to_mark( $end_mark, 0.0, TRUE, 0.0, 0.0 );
821             }
822             );
823              
824             $self->console($console);
825              
826             $scrolled_window->add($console);
827              
828             return $scrolled_window;
829             }
830              
831              
832             sub say {
833             my($self, $msg) = @_;
834              
835             my $console = $self->console;
836             my $buffer = $console->get_buffer;
837             my $end = $buffer->get_end_iter;
838             $buffer->insert ($end, $msg);
839             }
840              
841              
842             sub play_sound_file {
843             my($self, $sound_file) = @_;
844              
845             $sound_file ||= $self->selected_sound;
846              
847             if(-r $sound_file) {
848             system("play $sound_file >/dev/null 2>&1 &");
849             }
850             }
851              
852              
853             sub confirm_master_dialog {
854             my($self, $key_info) = @_;
855              
856             my $dialog = Gtk2::Dialog->new(
857             "USB Master Key",
858             $self->app_win,
859             [qw/modal destroy-with-parent/],
860             'gtk-cancel' => 'cancel',
861             'Read Master Key' => 'ok',
862             );
863             $dialog->set_default_size (90, 80);
864              
865             my $table = Gtk2::Table->new(1, 3, FALSE);
866              
867             my @pack_opts = ( ['expand', 'fill'], ['expand', 'fill'], 4, 2);
868             my $row = 0;
869              
870             my $v_label = Gtk2::Label->new;
871             $v_label->set_markup('<b>Vendor:</b>');
872             $v_label->set_alignment(0, 0.5);
873             $table->attach($v_label, 0, 1, $row, $row + 1, @pack_opts);
874              
875             my $v_value = Gtk2::Label->new($key_info->{vendor});
876             $v_value->set_alignment(0, 0.5);
877             $table->attach($v_value, 1, 2, $row, $row + 1, @pack_opts);
878             $row++;
879              
880             my $c_label = Gtk2::Label->new;
881             $c_label->set_markup('<b>Total Capacity:</b>');
882             $c_label->set_alignment(0, 0.5);
883             $table->attach($c_label, 0, 1, $row, $row + 1, @pack_opts);
884              
885             my $media_size = $key_info->{media_size};
886             1 while $media_size =~ s{^([-+]?\d+)(\d{3})}{$1,$2};
887             my $c_value = Gtk2::Label->new("$media_size bytes");
888             $c_value->set_alignment(0, 0.5);
889             $table->attach($c_value, 1, 2, $row, $row + 1, @pack_opts);
890             $row++;
891              
892             my $volume_label = $self->get_volume_label($key_info->{block_device});
893             if($volume_label) {
894             my $l_label = Gtk2::Label->new;
895             $l_label->set_markup('<b>Volume Label:</b>');
896             $l_label->set_alignment(0, 0.5);
897             $table->attach($l_label, 0, 1, $row, $row + 1, @pack_opts);
898              
899             my $l_value = Gtk2::Label->new($volume_label);
900             $l_value->set_alignment(0, 0.5);
901             $table->attach($l_value, 1, 2, $row, $row + 1, @pack_opts);
902             $row++;
903             }
904              
905             my $t_label = Gtk2::Label->new;
906             $t_label->set_markup('<b>Temp Folder:</b>');
907             $t_label->set_alignment(0, 0.5);
908             $table->attach($t_label, 0, 1, $row, $row + 1, @pack_opts);
909              
910             my $t_chooser = Gtk2::FileChooserButton->new(
911             'Select a folder', 'select-folder'
912             );
913             $t_chooser->set_filename('/tmp'); # TODO fixme!
914             $table->attach($t_chooser, 1, 2, $row, $row + 1, @pack_opts);
915             $row++;
916              
917             my $p_label = Gtk2::Label->new;
918             $p_label->set_markup('<b>Copying Profile:</b>');
919             $p_label->set_alignment(0, 0.5);
920             $table->attach($p_label, 0, 1, $row, $row + 1, @pack_opts);
921              
922             my $profile_combo = Gtk2::ComboBox->new_text;
923             my $profiles = $self->profiles;
924             my $selected = $self->selected_profile;
925             my @profile_names = sort keys %$profiles;
926             my $i = 0;
927             foreach my $key (@profile_names) {
928             next unless $profiles->{$key}->{reader};
929             $profile_combo->append_text($key);
930             $profile_combo->set_active($i) if $key eq $selected;
931             $i++;
932             }
933             $table->attach($profile_combo, 1, 2, $row, $row + 1, @pack_opts);
934             $row++;
935              
936             $table->show_all;
937             $dialog->vbox->pack_start($table, FALSE, FALSE, 4);
938              
939             my $result;
940             while(!$result or $result eq 'none') {
941             $result = $dialog->run;
942             }
943             my $temp_root = $t_chooser->get_filename;
944              
945             $dialog->destroy;
946             return if $result ne 'ok';
947              
948             $self->set_temp_root($temp_root);
949             $self->volume_label($volume_label);
950             $self->select_profile($profile_names[$profile_combo->get_active]);
951              
952             return TRUE;
953             }
954              
955              
956             sub get_volume_label {
957             my($self, $device) = @_;
958              
959             $device .= '1'; # examine first partition
960             my $command = $self->sudo_wrap("dosfslabel $device");
961             my $label = `$command 2>/dev/null`;
962             chomp($label) if defined $label;
963             return $label;
964             }
965              
966              
967             sub tick {
968             my $self = shift;
969              
970             my $exit_status = $self->exit_status;
971             return TRUE unless keys %$exit_status;
972              
973             my $state = $self->current_state;
974             if($state eq 'MASTER-COPYING') {
975             $self->master_copy_finished($exit_status);
976             }
977             elsif($state eq 'COPYING') {
978             $self->copy_finished($exit_status);
979             }
980             return TRUE;
981             }
982              
983              
984             sub master_copy_finished {
985             my($self, $exit_status) = @_;
986              
987             my $pid = $self->master_info->{pid} or return;
988             if(defined($exit_status->{$pid})) {
989             if($exit_status->{$pid} == 0) {
990             $self->current_state('MASTER-COPIED');
991             $self->say("Remove the master key.\n");
992             }
993             else {
994             $self->say("Failed to read master key. Please try again.\n");
995             $self->current_state('MASTER-WAIT');
996             }
997             }
998             }
999              
1000              
1001             sub copy_finished {
1002             my($self, $exit_status) = @_;
1003              
1004             my $current_keys = $self->current_keys;
1005             my %pid_to_udi = map {
1006             $current_keys->{$_}->{pid}
1007             ? ($current_keys->{$_}->{pid} => $_)
1008             : ();
1009             } keys %$current_keys;
1010              
1011             my $done = 0;
1012             foreach my $pid (keys %$exit_status) {
1013             my $status = delete $exit_status->{$pid};
1014             my $udi = $pid_to_udi{$pid} or next;
1015             if($status == 0) {
1016             $self->update_key_progress($udi, 10);
1017             }
1018             else {
1019             $self->update_key_progress($udi, -1);
1020             my $key_info = $current_keys->{$udi};
1021             my $output = $key_info->{output};
1022             $output =~ s/^{\d+\/\d+}\n//mg;
1023             $self->say("Copy to $key_info->{dev} failed:\n$output\n\n");
1024             }
1025             $done++;
1026             }
1027              
1028             if($done) {
1029             foreach my $key_info (values %$current_keys) {
1030             if($key_info->{status} >= 0 and $key_info->{status} < 10) {
1031             $done = 0;
1032             last;
1033             }
1034             }
1035             $self->play_sound_file if $done;
1036             }
1037             return TRUE;
1038             }
1039              
1040              
1041             sub set_temp_root {
1042             my($self, $new_temp) = @_;
1043              
1044             $self->clean_temp_dir;
1045              
1046             $self->temp_root($new_temp);
1047             my $temp_dir = "$new_temp/usb-copy.$$";
1048              
1049             my $path = "$temp_dir/master";
1050             $self->master_root($path);
1051             mkpath($path, { mode => 0700 }) if not -d $path;
1052              
1053             $path = "$temp_dir/mount";
1054             $self->mount_dir($path);
1055             mkpath($path, { mode => 0700 }) if not -d $path;
1056              
1057             return;
1058             }
1059              
1060              
1061             sub clean_temp_dir {
1062             my $self = shift;
1063              
1064             my $path = $self->master_root or return;
1065             $path =~ s{/master$}{};
1066             if(-d $path and $self->sudo_path and $self->current_state ne 'MASTER-WAIT') {
1067             my $command = $self->sudo_wrap("chown -R $< $path");
1068             system($command);
1069             }
1070             rmtree($path) if -d $path;
1071             }
1072              
1073              
1074             sub run {
1075             my $self = shift;
1076              
1077             # Arrange to catch exit status of child processes
1078             my $exit_status = $self->exit_status;
1079             $SIG{CHLD} = sub {
1080             my $pid;
1081             do {
1082             $pid = waitpid(-1, WNOHANG);
1083             $exit_status->{$pid} = $? if $pid > 0;
1084             } while $pid > 0;
1085             };
1086             Glib::Timeout->add(500, sub { $self->tick });
1087              
1088             Gtk2->main;
1089              
1090             $self->restore_automount;
1091             $self->clean_temp_dir;
1092             }
1093              
1094              
1095             1;
1096              
1097             __END__
1098              
1099             =head1 ATTRIBUTES
1100              
1101             The application object has the following attributes (with correspondingly named
1102             accessor methods):
1103              
1104             =over 4
1105              
1106             =item app_win
1107              
1108             The main Gtk2::Window object.
1109              
1110             =item automount_state
1111              
1112             Stores the enabled state ('true' or 'false') of the GNOME/Nautilus media
1113             automount option. The function will be disabled on startup and this value will
1114             be restored on exit.
1115              
1116             =item capacity_combo
1117              
1118             The Gtk2::ComboBox object for the device filter 'Capacity' drop-down menu.
1119              
1120             =item capacity_entry
1121              
1122             The Gtk2::Entry object for the device filter 'Capacity' text entry box.
1123              
1124             =item console
1125              
1126             The Gtk2::TextView object used for writing output messages.
1127              
1128             =item current_keys
1129              
1130             A hash for tracking which (non-master) keys are currently inserted and what
1131             stage each copy process is at. The hash key is the device 'UDI' and the value
1132             is a hash of device dtails .
1133              
1134             =item current_state
1135              
1136             Used to control which mode the application is in:
1137              
1138             MASTER-WAIT waiting for the user to insert the master key
1139             MASTER-COPYING waiting for the master key 'reader' script to complete
1140             MASTER-COPIED waiting for the user to remove the master key
1141             COPYING waiting for the user to insert blank keys
1142              
1143             =item exit_status
1144              
1145             Used by a SIGCHLD handler to track the exit status of the copy scripts. The
1146             key is a process ID and the value is the exist status returned by C<wait>.
1147              
1148             =item hal
1149              
1150             The DBus object ('org.freedesktop.Hal.Manager') from which device add/remove
1151             events are received.
1152              
1153             =item key_rack
1154              
1155             The Gtk2::HBox object containing the widgets representing currently inserted
1156             keys.
1157              
1158             =item master_info
1159              
1160             A hash of device details for the 'master' USB key.
1161              
1162             =item master_root
1163              
1164             The path to the temp directory containing the copy of the master key.
1165              
1166             =item mount_dir
1167              
1168             The path to the temp directory containing temporary mount points.
1169              
1170             The volume label read from the master key and to be applied to the copies.
1171              
1172             =item options
1173              
1174             A hash of option name/value pairs passed in from comman-line arguments by the
1175             wrapper script.
1176              
1177             =item profiles
1178              
1179             A hash of details of known profiles. Used to populate the profile drop-down
1180             menu on the confirm master key dialog.
1181              
1182             =item selected_profile
1183              
1184             The name of the copying profile which will be used to select reader/writer
1185             scripts.
1186              
1187             =item selected_sound
1188              
1189             Pathname of the currently selected sound file, to be played when copying is
1190             complete.
1191              
1192             =item sudo_path
1193              
1194             If the script was run by a non-root user and sudo is available, this string
1195             will be populated with the pathname of either C<gksudo> or C<sudo>. When
1196             running the read/writer scripts the string will be prepended onto the commands.
1197              
1198             =item temp_root
1199              
1200             The temp directory selected by the user. The application will create a
1201             subdirectory for the copy of the master key and for temporary mount points.
1202              
1203             =item vendor_combo
1204              
1205             The Gtk2::ComboBox object for the device filter 'Vendor' drop-down menu.
1206              
1207             =item vendor_entry
1208              
1209             The Gtk2::Entry object for the device filter 'Vendor' text entry box.
1210              
1211             =item volume_label
1212              
1213             The volume label which will be passed to the writer script.
1214              
1215             =back
1216              
1217             =head1 PROFILES
1218              
1219             The tasks of reading a master key and writing to a blank key are delegated to
1220             'reader' and 'writer' scripts. A pair of reader/writer scripts is supplied but
1221             the application also supports using different scripts as dictated by a user
1222             selection. The supplied scripts assume file-by-file copying and format the
1223             blank keys with a VFAT filesystem. An alternate script might for example, use
1224             C<dd> to write a complete filesystem image in a single operation.
1225              
1226             A pair of scripts is referred to as a copying 'profile'. The user can select a
1227             profile via a command-line option or from a drop-down list when confirming the
1228             master key.
1229              
1230             The supplied scripts are called:
1231              
1232             copyfiles-reader.sh
1233             copyfiles-writer.sh
1234              
1235             A profile does not need to include a reader script. If a profile which only
1236             includes a writer script is selected (via the command-line options) then the
1237             application will go immediately into the mode of waiting for blank keys.
1238              
1239             =head2 Profile Script API
1240              
1241             The filename of the reader script must end with C<-reader> (followed by an
1242             optional extension) and similarly, the filename of the writer script must end
1243             with C<-writer>.
1244              
1245             The reader/writer scripts do not have to be shell scripts - they merely need to
1246             be executable. The application ignores the file extension if it is present.
1247              
1248             Both reader and writer scripts are assumed to have succeeded if they have an
1249             exit status of 0. A non-zero exit status will be considered a failure.
1250              
1251             When the master key reader script is invoked, the following environment
1252             variables will be set:
1253              
1254             USB_BLOCK_DEVICE e.g.: /dev/sdb
1255             USB_MOUNT_DIR e.g.: /tmp/usb-copy.nnnnn/mount/sdb
1256             USB_MASTER_ROOT e.g.: /tmp/usb-copy.nnnnn/master
1257              
1258             The writer script will be passed the same set of variables and one extra:
1259              
1260             USB_VOLUME_NAME e.g.: FREE-STUFF
1261              
1262             Be warned that this variable may be empty - depending on what was returned from
1263             running C<dosfslabel> against the master key. It is entirely reasonable for a
1264             custom writer script to ignore this variable altogether and either use a
1265             hardcoded volume label or not use one at all.
1266              
1267             The writer script can also indicate progress (for updating the progress bar in
1268             the icon) by writing lines to STDOUT in the following format:
1269              
1270             {x/y}
1271              
1272             Where '{' is the first character on a line; 'x' is an integer indicating the
1273             number of steps completed; and 'y' is an integer indicating the total number
1274             of steps. For example if the script output this line:
1275              
1276             {4/8}
1277              
1278             the status icon would be updated to indicate 50% complete.
1279              
1280             =head1 METHODS
1281              
1282             =head2 Constructor
1283              
1284             The C<new> method is used to create an application object. It in turn calls
1285             C<BUILD> to create and populate the application window and hook into HAL (the
1286             Hardware Abstraction Layer) via DBus to get notifications of devices been
1287             added/removed.
1288              
1289             =head2 add_key_to_rack ( key_info )
1290              
1291             Called from C<hal_device_added> if the newly added device matches the current
1292             device filter settings. The C<key_info> parameter supplied is a hashref of
1293             device properties as returned by C<hal_device_properties>. A GUI widget
1294             representing the new USB key is added to the user interface and a data
1295             structure to track the copying process is created.
1296              
1297             =head2 build_console ( )
1298              
1299             Called from C<build_ui> to create the scrolled text window for displaying
1300             progress messages.
1301              
1302             =head2 build_filters ( )
1303              
1304             Called from C<build_ui> to create the toolbar of drop-down menus and text
1305             entries for the device filter settings.
1306              
1307             =head2 build_key_rack ( )
1308              
1309             Called from C<build_ui> to create the container widget to house the
1310             per-key status indicators.
1311              
1312             =head2 build_menu ( )
1313              
1314             Called from C<build_ui> to create the application menu and hook the menu
1315             items up to handler methods.
1316              
1317             =head2 build_ui ( )
1318              
1319             Called from the constructor to create the main application window and populate
1320             it with Gtk widgets.
1321              
1322             =head2 check_for_root_user ( )
1323              
1324             Called on startup to check that either the script is running as root or that sudo
1325             is available. In the latter case, sudo (or gksudo) will be used to invoke the
1326             read/writer scripts.
1327              
1328             If the script is not running with root permissions; and sudo is not available;
1329             and the C<--no-root-check> option was not specified, this method will die with
1330             an appropriate error message.
1331              
1332             =head2 clean_temp_dir ( )
1333              
1334             Called from the C<run> method immediately before the application exits. This
1335             method is responsible for removing the temporary directories containing the
1336             master copy of the files and the mount points for the blank keys.
1337              
1338             When running as a non-root user, this method needs to use sudo in order to
1339             remove the files created by the reader script when it was running as root.
1340              
1341             =head2 commandline_options ( )
1342              
1343             This B<class> method returns a list of recognised options in the form expected
1344             by L<Getopt::Long>.
1345              
1346             =head2 confirm_master_dialog ( key_info )
1347              
1348             This method is called each time a USB key is inserted when the application is
1349             in the C<MASTER-WAIT> state. The C<key_info> parameter supplied is a hashref
1350             of device properties as returned by C<hal_device_properties>. this method
1351             displays a dialog box to allow the user to confirm that the device should be
1352             used as the master key.
1353              
1354             If the user selects 'Cancel', no further action is taken and the application
1355             goes back to waiting for a master key to be inserted.
1356              
1357             If the user confirms the device should be used as the master, then control is
1358             passed to the C<start_master_read> method.
1359              
1360             =head2 copy_finished ( exit_status )
1361              
1362             Called when a 'writer' process exits. Checks the exit status and updates the
1363             icon in the key rack (0 = success, non-zero = failure).
1364              
1365             =head2 disable_automount ( )
1366              
1367             This method is called at startup to query GConf for the current GNOME/Nautilus
1368             media automount status ('true'/'false' for enabled/disabled). The current
1369             state is saved and then the value is set to false. The operation should fail
1370             silently in non-GNOME environments.
1371              
1372             =head2 disable_filter_inputs ( )
1373              
1374             This method is called from C<require_master_key> to disable the menu and text
1375             entry widgets on the device filter toolbar.
1376              
1377             =head2 enable_filter_inputs ( )
1378              
1379             This method is called from C<require_master_key> to enable the menu and text
1380             entry widgets on the device filter toolbar.
1381              
1382             =head2 find_command ( command )
1383              
1384             Takes a command name and returns the path to the first matching executable file
1385             found in a directory listed in the $PATH environment variable. Returns
1386             C<undef> if no match found.
1387              
1388             =head2 fork_copier ( key_info )
1389              
1390             Called from C<add_key_to_rack>. Forks a 'writer' process and collects its
1391             STDOUT+STDERR via a pipe.
1392              
1393             =head2 get_volume_label ( device )
1394              
1395             Called from C<confirm_master_dialog> when collecting information about the key
1396             which was just inserted. Current implementation simply runs the C<dosfslabel>
1397             command.
1398              
1399             =head2 hal_device_added ( udi )
1400              
1401             Called to handle a 'DeviceAdded' event from HAL via DBus. Delegates to
1402             C<start_master_read> if the app is waiting for a master key. Otherwise checks
1403             whether the new device parameters match the current filter settings and
1404             delegates to C<add_key_to_rack> if they do.
1405              
1406             =head2 hal_device_properties ( udi )
1407              
1408             Called from C<hal_device_added> to query HAL. Returns a hash(ref) of device
1409             details. The global variable C<%hal_device_added> defines which attributes
1410             returned from HAL will appear in the hash and which keys they will be mapped
1411             to.
1412              
1413             =head2 hal_device_removed ( udi )
1414              
1415             Called to handle a 'DeviceRemoved' event from HAL via DBus. Delegates to
1416             C<remove_key_from_rack> if the application is in the C<COPYING> state.
1417              
1418             =head2 init_dbus_watcher ( )
1419              
1420             Called from the constructor to hook up device-add events to the
1421             C<hal_device_added> method and device-remove events to C<hal_device_removed>.
1422              
1423             =head2 master_copy_finished ( exit_status )
1424              
1425             Called when the 'reader' process exits. Checks the exit status and updates the
1426             application state to <MASTER-COPIED> on success or C<MASTER-WAIT> on failure.
1427              
1428             =head2 match_device_filter ( key_info )
1429              
1430             Called from C<hal_device_added> and returns true if the device matches the
1431             current filter parameters, or false otherwise.
1432              
1433             =head2 on_copier_pipe_read ( fileno, condition, udi )
1434              
1435             Handler for data received from a 'writer' process. Updates the status icon for
1436             the device to indicate progress.
1437              
1438             =head2 on_master_pipe_read ( fileno, condition, udi )
1439              
1440             Handler for data received from the master key 'reader' process. Copies output
1441             from the process to the console widget.
1442              
1443             =head2 on_menu_edit_preferences ( )
1444              
1445             Handler for the Edit E<gt> Preferences menu item - not currently implemented.
1446              
1447             =head2 on_menu_file_new ( )
1448              
1449             Handler for the File E<gt> New menu item. Resets the application state via
1450             C<require_master_key>.
1451              
1452             =head2 on_menu_file_quit ( )
1453              
1454             Handler for the File E<gt> Quit menu item. Exits the Gtk event loop, which
1455             returns control to the C<run> method.
1456              
1457             =head2 on_menu_help_about ( )
1458              
1459             Handler for the Help E<gt> About menu item. Displays 'About' dialog.
1460              
1461             =head2 play_sound_file ( sound_file )
1462              
1463             This method takes a pathname to a sound file (e.g.: a .wav) and plays it.
1464             The current implementation simply runs the the SOX C<play> command - it should probably use GStreamer
1465              
1466             =head2 reader_script ( )
1467              
1468             Returns the path to the script from the currently selected profile, which will
1469             be used to read the master key. Will return undef if the selected profile does
1470             not include a reader script.
1471              
1472             =head2 ready_to_write ( )
1473              
1474             This method is called after the master key has been read (or immediately on
1475             startup if the selected profile does not use a reader script) and puts the
1476             application into the mode of waiting for blank keys to be inserted.
1477              
1478             =head2 remove_key_from_rack ( udi )
1479              
1480             Called from C<hal_device_removed> to remove the indicator widget corresponding
1481             to the USB key which has just been removed.
1482              
1483             =head2 require_master_key ( )
1484              
1485             Called from the constructor to put the app in the C<MASTER-WAIT> mode (waiting
1486             for the master key to be inserted). Can also be called from the
1487             C<on_menu_file_new> menu event handler.
1488              
1489             =head2 restore_automount ( )
1490              
1491             This method is called at exit time restore the original GConf setting for the
1492             GNOME/Nautilus media automount function.
1493              
1494             =head2 run ( )
1495              
1496             This method is called from the wrapper script. It's job is to run the Gtk
1497             event loop and when that exits, to call C<clean_temp_dir> and then return.
1498              
1499             =head2 say ( message )
1500              
1501             Appends a message to the console widget. (Note, the caller is responsible
1502             for supplying the newline characters).
1503              
1504             =head2 scan_for_profiles ( )
1505              
1506             Populates the hash of profile data in the C<profiles> attribute.
1507              
1508             =head2 select_profile ( profile_name )
1509              
1510             This method is used to select which reader/writer scripts will be used. At
1511             present there is one hard-coded call to this method in the constructor.
1512             Ideally, the user would select from all available profile scripts in the
1513             'confirm master' dialog.
1514              
1515             =head2 set_temp_root ( pathname )
1516              
1517             Called from C<confirm_master_dialog> based on the temp directory selected by
1518             the user.
1519              
1520             =head2 start_master_read ( key_info )
1521              
1522             Called from C<hal_device_added> to fork off a 'reader' process to slurp in the
1523             contents of the master key.
1524              
1525             =head2 sudo_wrap ( command env-var-names )
1526              
1527             If the script is run by a non-root user and sudo is available and the
1528             C<--no-root-check> option was not specified, this method will return a command
1529             string which wraps the supplied command in a call to either C<gksudo> or
1530             C<sudo>. For all other cases, C<command> is returned unmodified.
1531              
1532             The C<gksudo> command is preferred since it gives the user a GUI prompt window
1533             if it is necessary to prompt for a password. This method handles the different
1534             semantics required to pass environment variables through C<gksudo> and C<sudo>.
1535              
1536             =head2 tick ( )
1537              
1538             This timer event handler is used to take the child process exit status values
1539             collected by the SIGCHLD handler and pass them to C<master_copy_finished> or
1540             C<copy_finished> as appropriate.
1541              
1542             =head2 update_key_progress ( udi, status )
1543              
1544             Called from C<on_copier_pipe_read> to update the status icon for a specified
1545             USB key device. The progress parameter is a number in the range 0-10 for
1546             copies in progress; -1 for a copy that has failed (non-zero exit status from
1547             the 'writer' process); or -2 to indicate a device which did not match the
1548             filter settings and is being ignored.
1549              
1550             =head2 writer_script ( )
1551              
1552             Returns the path to the script from the currently selected profile, which will
1553             be used to write to the blank keys.
1554              
1555             =cut
1556              
1557             =head1 AUTHOR
1558              
1559             Grant McLean, C<< <grantm at cpan.org> >>
1560              
1561             =head1 BUGS
1562              
1563             Please report any bugs or feature requests to C<bug-app-usbkeycopycon at rt.cpan.org>, or through
1564             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=App-USBKeyCopyCon>. I will be notified, and then you'll
1565             automatically be notified of progress on your bug as I make changes.
1566              
1567              
1568             =head1 SUPPORT
1569              
1570             You can find documentation for this module with the perldoc command.
1571              
1572             perldoc App::USBKeyCopyCon
1573              
1574              
1575             You can also look for information at:
1576              
1577             =over 4
1578              
1579             =item * github: source code repository
1580              
1581             L<http://github.com/grantm/usb-key-copy-con>
1582              
1583             =item * RT: CPAN's request tracker
1584              
1585             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=App-USBKeyCopyCon>
1586              
1587             =item * AnnoCPAN: Annotated CPAN documentation
1588              
1589             L<http://annocpan.org/dist/App-USBKeyCopyCon>
1590              
1591             =item * CPAN Ratings
1592              
1593             L<http://cpanratings.perl.org/d/App-USBKeyCopyCon>
1594              
1595             =item * Search CPAN
1596              
1597             L<http://search.cpan.org/dist/App-USBKeyCopyCon>
1598              
1599             =back
1600              
1601              
1602             =head1 COPYRIGHT & LICENSE
1603              
1604             Copyright 2009 Grant McLean, all rights reserved.
1605              
1606             This program is free software; you can redistribute it and/or modify it
1607             under the same terms as Perl itself.
1608              
1609              
1610             =cut
1611              
1612