File Coverage

blib/lib/POEx/Inotify.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             ## $Id$
2             #####################################################################
3             package POEx::Inotify;
4              
5 1     1   43228 use 5.008008;
  1         4  
  1         41  
6 1     1   5 use strict;
  1         2  
  1         35  
7 1     1   5 use warnings;
  1         2  
  1         68  
8              
9             our $VERSION = '0.0201';
10             $VERSION = eval $VERSION; # see L
11              
12 1     1   738 use POE;
  0            
  0            
13             use POE::Session::PlainCall;
14             use Storable qw( dclone );
15             use Carp;
16             use Data::Dump qw( pp );
17              
18             use Linux::Inotify2;
19              
20             sub DEBUG () { 0 }
21             sub DEBUG2 () { 0 }
22              
23             #############################################
24             sub spawn
25             {
26             my( $package, %init ) = @_;
27              
28             my $options = delete $init{options};
29             $options ||= {};
30              
31             POE::Session::PlainCall->create(
32             package => $package,
33             ctor_args => [ \%init ],
34             options => $options,
35             states => [ qw( _start _stop shutdown
36             poll inotify
37             monitor unmonitor
38             __pending_deleted __pending_created
39             __self_deleted
40             ) ]
41             );
42             }
43              
44              
45             #############################################
46             sub new
47             {
48             my( $package, $args ) = @_;
49              
50             my $self = bless {
51             path=>{} # path => $notifies
52             }, $package;
53             $self->{alias} = $args->{alias} || 'inotify';
54             $self->build_inotify;
55             return $self;
56             }
57              
58             #############################################
59             sub _start
60             {
61             my( $self ) = @_;
62             DEBUG and warn "$self->{alias}: _start";
63             poe->kernel->alias_set( $self->{alias} );
64             poe->kernel->sig( shutdown => 'shutdown' );
65             $self->setup_inotify;
66             }
67              
68             #############################################
69             sub _stop
70             {
71             my( $self ) = @_;
72             DEBUG and warn "$self->{alias}: _stop";
73             }
74              
75             #############################################
76             sub shutdown
77             {
78             my( $self ) = @_;
79             DEBUG and
80             warn "$self->{alias}: shutdown";
81             $self->{shutdown} = 1;
82             foreach my $path ( keys %{ $self->{path} } ) {
83             local $self->{force} = 1;
84             $self->unmonitor( { path=>$path } );
85             }
86             poe->kernel->select_read( $self->{fh} ) if $self->{fh};
87             poe->kernel->alias_remove( $self->{alias} );
88             delete $self->{fh};
89             # delete $self->{inotify};
90             return;
91             }
92              
93             #############################################
94             sub build_inotify
95             {
96             my( $self ) = @_;
97             $self->{inotify} = Linux::Inotify2->new;
98             }
99              
100             #############################################
101             sub setup_inotify
102             {
103             my( $self ) = @_;
104             $self->{inotify}->blocking( 0 );
105             $self->{fh} = IO::Handle->new_from_fd( $self->{inotify}->fileno, "r" );
106             poe->kernel->select_read( $self->{fh}, 'poll' );
107             }
108              
109             sub add_inotify
110             {
111             my( $self, $path, $mask ) = @_;
112             DEBUG and warn sprintf "$self->{alias}: mask=%08x path=$path", $mask;
113             return $self->{inotify}->watch( $path, $mask,
114             poe->session->callback( inotify=>$path ) );
115             }
116              
117             #############################################
118             # Poll the Inotify object
119             sub poll
120             {
121             my( $self ) = @_;
122             return if $self->{shutdown};
123             DEBUG and warn "$self->{alias}: poll";
124             $self->{inotify}->poll
125             }
126              
127             #############################################
128             # Callback from Inotify object
129             sub inotify
130             {
131             my( $self, $N, $E ) = @_;
132             my $notify = $self->_find_path( $N->[0] );
133             next unless $notify;
134              
135             foreach my $e ( @$E ) {
136             DEBUG and do {
137             warn "$self->{alias}: inotify ", $e->fullname;
138             foreach my $flag ( qw( ACCESS MODIFY ATTRIB CLOSE_WRITE CLOSE_NOWRITE
139             OPEN MOVED_FROM MOVED_TO CREATE DELETE DELETE_SELF
140             MOVE_SELF ONESHOT ONLYDIR DONT_FOLLOW
141             MASK_ADD CLOSE MOVE ) ) {
142             my $method = "IN_$flag";
143             warn "$self->{alias}: IN_$flag" if $e->$method();
144             }
145             };
146              
147             foreach my $call ( @{ $notify->{call} } ) {
148             DEBUG and
149             warn sprintf "$self->{alias}: %08x vs %08x", $e->mask, $call->{tmask};
150            
151             next unless $e->mask & $call->{tmask};
152              
153             my $CB = dclone $call->{cb};
154             $CB->[2] = $e;
155             poe->kernel->call( @$CB );
156             }
157             }
158             }
159              
160             #############################################
161             sub _find_path
162             {
163             my( $self, $path ) = @_;
164             return $self->{path}{ $path };
165             }
166              
167              
168             sub _build_calls
169             {
170             my( $self, $args ) = @_;
171              
172             unless( $args->{events} ) {
173             return "No event specified" unless $args->{event};
174             my $event = delete $args->{event};
175             my $mask = delete $args->{mask};
176             my $A = delete $args->{args};
177              
178             $mask = IN_ALL_EVENTS unless defined $mask;
179             $args->{events} = { $mask => { event=>$event,
180             args => $A
181             } };
182             }
183              
184             my $total_mask = 0;
185             my @calls;
186              
187             foreach my $mask ( keys %{ $args->{events} } ) {
188             $total_mask |= 0+$mask;
189              
190             my $E = $args->{events}{ $mask };
191              
192             my( $event, $A );
193             my $r = ref $E;
194             unless( $r ) { # { MASK => 'event' }
195             $event = $E;
196             $A = [];
197             }
198             elsif( 'ARRAY' eq $r ) { # { MASK => ['event', @ARGS }
199             $event = shift @$E;
200             $A = $E;
201             }
202             else { # { MASK => { event=>'event', args=>[] }
203             $event = $E->{event};
204             $A = $E->{args}||$args->{args};
205             }
206             # undef is place holder for the change object
207             my $call = [ $args->{session}, $event, undef ];
208              
209             push @calls, { cb => $call, # list of callbacks
210             mask => $mask, # user specified mask
211             tmask => $self->_const2mask( $mask, $args ), # true mask
212             mode => $args->{mode} # mode we want for this
213             };
214             next unless $A;
215              
216             $A = dclone $A if ref $A;
217             if( 'ARRAY' eq ref $A ) {
218             push @$call, @$A;
219             }
220             else {
221             push @$call, $A;
222             }
223             }
224             return "No event specified" unless @calls;
225              
226             return $total_mask, @calls;
227             }
228              
229             sub _const2mask
230             {
231             my( $self, $mask, $args ) = @_;
232             if( -f $args->{path} and $mask | IN_DELETE ) {
233             $mask |= IN_DELETE_SELF; # IN_DELETE is useless on a file
234             }
235             return $mask;
236             }
237              
238             #############################################
239             sub monitor
240             {
241             my( $self, $args ) = @_;
242             return if $self->{shutdown};
243             $args->{session} = poe->sender;
244              
245             my $mode = $args->{mode} ||= 'cooked';
246              
247             my $caller = join ' ', at => poe->caller_file,
248             line => poe->caller_line . "\n";
249              
250             my( $new_mask, @calls ) = $self->_build_calls( $args );
251             die "Nothing to do: $new_mask $caller" unless @calls;
252              
253             return $self->_monitor_add( $args->{path}, $mode, \@calls, $caller );
254             }
255              
256             #############################################
257             sub _monitor_add
258             {
259             my( $self, $path, $mode, $calls, $caller ) = @_;
260             confess "Why no calls? calls=", pp $calls unless $calls and 'ARRAY' eq ref $calls;
261             $caller ||= '';
262             if( !-e $path ) {
263             if( $mode eq 'cooked' ) {
264             $self->_pending( $path, $calls ) and return 1;
265             }
266             return;
267             }
268              
269             my $notify = $self->_find_path( $path );
270              
271             # save the new calls
272             if( $notify ) {
273             DEBUG and
274             warn "$self->{alias}: monitor $path again for $mode ($notify) $caller";
275             push @{ $notify->{call} }, @$calls;
276             }
277             else {
278             $notify = {
279             path => $path,
280             call => [ @$calls ],
281             mask => 0,
282             # watch =>
283             };
284             DEBUG and
285             warn "$self->{alias}: monitor $path for $mode ($notify) $caller";
286             $self->{path}{$path} = $notify;
287             DEBUG2 and warn "$self->{alias}: REFCNT PLUS ", poe->session->ID, " (me) $path";
288             poe->kernel->refcount_increment( poe->session->ID, "NOTIFY $path" );
289              
290             $self->_self_monitor( $path ) unless $mode eq 'raw'
291             or $mode =~ /^_/;
292             }
293              
294             $notify->{new_mask} = $self->_notify_mask( $notify );
295              
296             $notify->{watch} = $self->add_inotify( $path, $notify->{new_mask} );
297             die "Unable to watch $path: $! $caller" unless $notify->{watch};
298              
299             # And increment the sender's refcnt
300             foreach my $call ( @$calls ) {
301             use Carp;
302             use Data::Dump qw( pp );
303             confess pp $calls if 'ARRAY' eq ref $call;
304             DEBUG2 and warn "$self->{alias}: REFCNT PLUS ", $call->{cb}[0], " ($call->{mode}) $path";
305             DEBUG2 and $call->{mode} eq 'self' and warn "$self->{alias}: REFCNT $caller";
306             poe->kernel->refcount_increment( $call->{cb}[0], "NOTIFY $path" );
307             }
308             return 1;
309             }
310              
311              
312             #############################################
313             sub unmonitor
314             {
315             my( $self, $args ) = @_;
316             my $path = $args->{path};
317             $args->{session} = poe->sender;
318             $args->{mask} = 0xFFFFFFFF unless defined $args->{mask};
319             my $caller = join ' ', at => poe->caller_file,
320             line => poe->caller_line . "\n";
321              
322             DEBUG and
323             warn "$self->{alias}: Unmonitor $path $caller";
324              
325             my $once = 0;
326             my $notify = $self->_find_path( $path );
327             if( $notify ) {
328             $self->_unmonitor_remove( $path, $notify, $args, $caller );
329             $once++;
330             }
331            
332             my $P = $self->_find_pending( $path );
333             if( $P ) {
334             $self->_pending_remove( $path, $P, $args, $caller );
335             $once++;
336             }
337              
338             unless( $once or $self->{shutdown} ) {
339             warn "$self->{alias}: $path wasn't monitored $caller";
340             }
341             return $once;
342             }
343              
344             sub _unmonitor_remove
345             {
346             my( $self, $path, $notify, $args, $caller ) = @_;
347              
348             # Go through the calls, dropping those the sender wants us to drop
349             my $ours = 0;
350             my( @calls, @dec );
351             foreach my $call ( @{ $notify->{call} } ) {
352             if( $self->_call_match( $call, $args ) ) {
353             push @dec, $call;
354             }
355             else {
356             $ours++ if $call->{cb}[0] == poe->session->ID;
357             push @calls, $call;
358             }
359             }
360              
361             my $finished = 0;
362             if( @calls > $ours ) {
363             # If we have any non-internal calls, we keep the notify
364             DEBUG and
365             warn "$self->{alias}: still monitor $path";
366             $notify->{call} = \@calls;
367             if( @dec ) {
368             # If we found a CB to remove, that means the mask might have changed
369             $notify->{mask} = $self->_notify_mask( $notify );
370             $self->add_inotify( $path, $notify->{mask} );
371             }
372             }
373             else {
374             # No external calls so we can drop this notify
375             DEBUG and
376             warn "$self->{alias}: unmonitor $path";
377             $notify->{watch}->cancel;
378             delete $notify->{watch};
379             DEBUG2 and warn "$self->{alias}: REFCNT MINUS ", poe->session->ID, " (me) $path";
380             poe->kernel->refcount_decrement( poe->session->ID, "NOTIFY $path" );
381             delete $self->{path}{ $path };
382             $self->_self_unmonitor( $path );
383             push @dec, @calls;
384             $finished = 1;
385             }
386              
387             # Now clear the refcnt for the sender
388             foreach my $call ( @dec ) {
389             DEBUG2 and warn "$self->{alias}: REFCNT MINUS ", $call->{cb}[0], " ($call->{mode}) $path";
390             poe->kernel->refcount_decrement( $call->{cb}[0], "NOTIFY $path" );
391             }
392             return $finished;
393             }
394              
395             sub _call_match
396             {
397             my( $self, $call, $args ) = @_;
398             return 1 if $self->{force};
399             return unless $call->{cb}[0] == $args->{session};
400             my @E;
401            
402             # Which event do we want to unmonitor
403             if( $args->{event} ) { # event => 'event'
404             @E = ( $args->{event} );
405             }
406             elsif( $args->{events} ) {
407             my $r = ref $args->{events};
408             if( 'ARRAY' eq ref $r ) { # events => [ ... ]
409             @E = @{ $args->{event} };
410             }
411             elsif( 'HASH' eq $r ) { # events => { mask => 'event' }
412             while( my( $mask, $event ) = each %{ $args->{event} } ) {
413             next unless $call->{mask} & $mask;
414             push @E, $event;
415             }
416             return 0 unless @E; # no mask matched
417             }
418             }
419             return 1 unless @E; # all of them for this session?
420              
421             # only some of them
422             foreach my $event ( @E ) {
423             return 1 if $event eq '*';
424             return 1 if $event eq $call->{cb}[1];
425             }
426            
427             return;
428             }
429              
430              
431             sub _notify_mask
432             {
433             my( $self, $notify ) = @_;
434             my $mask = 0;
435             foreach my $call ( @{ $notify->{call} } ) {
436             confess pp $notify if 'ARRAY' eq ref $call;
437             $mask |= $call->{mask};
438             }
439             return $mask;
440             }
441              
442              
443              
444              
445             #####################################################################
446             sub _pending
447             {
448             my( $self, $path, $calls ) = @_;
449              
450             my $P = $self->_find_pending( $path );
451             if( $P and $P->{monitored} ) {
452             DEBUG and warn "$self->{alias}: pending $path more";
453             push @{ $P->{call} }, @$calls if $calls;
454             return 1;
455             }
456              
457             my @todo = File::Spec->splitdir( $path );
458             while( @todo > 1 ) {
459             my $want = pop @todo;
460             my $maybe = File::Spec->catdir( @todo );
461             if( -e $maybe ) {
462             if( $self->_pending_monitor( $path, $maybe, $want ) ) {
463             DEBUG and warn "$self->{alias}: want $want in $maybe";
464             my $P = $self->_find_pending( $path );
465             $P->{monitored} = 1;
466             push @{ $P->{call} }, @$calls if $calls;
467             return;
468             }
469             }
470             }
471             return $self->_pending_monitor( $path, File::Spec->rootdir, @todo, $calls );
472             }
473              
474             #############################################
475             sub _pending_remove
476             {
477             my( $self, $path, $P, $args, $caller ) = @_;
478              
479             my( @calls, @dec );
480             foreach my $call ( @{ $P->{call} } ) {
481             next if $self->_call_match( $call, $args );
482             push @calls, $call;
483             }
484              
485             my $finished = 0;
486             if( @calls ) {
487             DEBUG and
488             warn "$self->{alias}: still pending $path ($P)";
489             $P->{call} = \@calls;
490             }
491             else {
492             # No external calls so we can drop this notify
493             DEBUG and
494             warn "$self->{alias}: unpending $path ($P)";
495             unless( $self->_pending_N( $path, $P->{exists} ) ) {
496             $self->_pending_unmonitor( $path, $P->{exists} );
497             }
498            
499             delete $self->{pending}{ $path };
500             $finished = 1;
501             }
502              
503             return $finished;
504             }
505              
506             #############################################
507             sub _pending_monitor
508             {
509             my( $self, $path, $exists, $want ) = @_;
510             DEBUG and
511             warn "$self->{alias}: pending monitor $exists";
512              
513             $self->{pending}{ $path } ||= { call=>[] };
514             $self->{pending}{ $path }{exists} = $exists;
515              
516             my $M = { path => $exists,
517             mode => '_pending',
518             events => {
519             (IN_DELETE_SELF|IN_MOVE_SELF) =>
520             [ '__pending_deleted', $path, $exists ],
521             (IN_MOVED_TO|IN_CREATE|IN_CLOSE_WRITE) =>
522             [ '__pending_created', $path, $exists, $want ]
523             }
524             };
525             # this has to be a call bacause ->monitor calls poe->sender
526             return poe->kernel->call( $self->{alias}, 'monitor', $M );
527             }
528              
529              
530             sub _pending_unmonitor
531             {
532             my( $self, $path, $exists ) = @_;
533              
534             my $P = $self->_find_pending( $path );
535             return unless $P and $P->{monitored};
536             DEBUG and
537             warn "$self->{alias}: pending unmonitor $exists";
538             poe->kernel->call( $self->{alias}, 'unmonitor',
539             { path=>$exists,
540             events => [ qw( __pending_deleted __pending_created ) ]
541             } );
542             $P->{monitored} = 0;
543             }
544              
545             #############################################
546             sub _find_pending
547             {
548             my( $self, $path ) = @_;
549             return $self->{pending}{ $path };
550             }
551              
552             sub _pending_N
553             {
554             my( $self, $path, $exists ) = @_;
555             foreach my $kpath ( keys %{ $self->{pending} } ) {
556             next if $kpath eq $path;
557             return 1 if $self->{pending}{ $kpath }{exists} eq $exists;
558             }
559             return 0;
560             }
561              
562             #############################################
563             sub __pending_deleted
564             {
565             my( $self, $ch, $path, $exists ) = @_;
566              
567             my $P = $self->_find_pending( $path );
568             return unless $P;
569              
570             DEBUG and
571             warn "$self->{alias}: pending deleted $exists";
572             unless( $self->_pending_N( $path, $exists ) ) {
573             $self->_pending_unmonitor( $path, $exists );
574             }
575             else {
576             $P->{monitored} = 0;
577             }
578             $self->_pending( $path );
579             }
580              
581             #############################################
582             sub __pending_created
583             {
584             my( $self, $ch, $path, $exists, $want ) = @_;
585             return unless $ch->name eq $want;
586              
587             my $P = $self->_find_pending( $path );
588             return unless $P;
589              
590             DEBUG and warn "$self->{alias}: pending $path created ", $ch->name;
591             unless( $self->_pending_N( $path, $exists ) ) {
592             $self->_pending_unmonitor( $path, $exists );
593             }
594             else {
595             $P->{monitored} = 0;
596             }
597             delete $self->{pending}{ $path };
598             if( $self->_monitor_add( $path, 'cooked', $P->{call} ) ) {
599             $self->_fake_created( $path );
600             }
601             }
602              
603              
604              
605              
606              
607             #####################################################################
608             sub _self_monitor
609             {
610             my( $self, $path ) = @_;
611             my $S = $self->_find_self( $path );
612             if( $S and $S->{monitored} ) {
613             DEBUG and warn "$self->{alias}: check $path more";
614             return;
615             }
616              
617             $S = $self->{self}{ $path } = {};
618              
619             my $M = { path => $path,
620             mask => (IN_MOVE_SELF|IN_DELETE_SELF),
621             event => '__self_deleted',
622             mode => '_self',
623             args => [ $path ]
624             };
625              
626             $S->{monitored} = poe->kernel->call( $self->{alias} => 'monitor', $M );
627             return 1 if $S->{monitored};
628             warn "$self->{alias}: Monitoring $path failed";
629             delete $self->{self}{ $path };
630             return;
631             }
632              
633             #############################################
634             sub _self_remove
635             {
636             my( $self, $path, $S, $args, $caller ) = @_;
637              
638             my( @calls, @dec );
639             foreach my $call ( @{ $S->{call} } ) {
640             next if $self->_call_match( $call, $args );
641             push @calls, $call;
642             }
643              
644             my $finished = 0;
645             if( @calls ) {
646             DEBUG and
647             warn "$self->{alias}: still self $path";
648             $S->{call} = \@calls;
649             }
650             else {
651             DEBUG and
652             warn "$self->{alias}: unself $path";
653             $self->_self_unmonitor( $path );
654             $finished = 1;
655             }
656              
657             return $finished;
658             }
659              
660            
661             #############################################
662             sub _self_unmonitor
663             {
664             my( $self, $path ) = @_;
665             delete $self->{self}{ $path };
666             # we are called from ->unmonitor which has already cleared {path}
667             # and the Inotify2 object, so we don't have to do anything more
668              
669             # But if we do add an 'unmonitor' call here, then we must add a _self_N
670             # to make sure we only unmonitor when no one is interested
671             }
672              
673             #############################################
674             sub __self_deleted
675             {
676             my( $self, $ch, $path ) = @_;
677             DEBUG and warn "$self->{alias}: check $path deleted";
678              
679             return if $self->{shutdown};
680              
681             my $P = $self->_find_path( $path );
682              
683             local $self->{force} = 1; # delete everything for this path. It no longer exists!
684             $self->unmonitor( { path=>$path } );
685              
686             my @keep;
687             my $changed;
688             foreach my $call ( @{ $P->{call} } ) {
689             if( $call->{mode} eq 'cooked' ) {
690             push @keep, $call;
691             }
692             else {
693             $changed = 1;
694             }
695             }
696             if( @keep ) {
697             $self->_pending( $path, \@keep );
698             }
699             }
700              
701             #############################################
702             sub _find_self
703             {
704             my( $self, $path ) = @_;
705             return $self->{self}{ $path };
706             }
707              
708              
709             #####################################################################
710             sub _fake_created
711             {
712             my( $self, $path ) = @_;
713             my $notify = $self->_find_path( $path );
714             return unless $notify;
715             my $ch = bless { mask => IN_CREATE,
716             cookie => 0,
717             name => '',
718             w => $notify->{watch}
719             }, 'Linux::Inotify2::Event';
720             poe->kernel->post( $self->{alias}, 'inotify', [ $path ], [ $ch ] );
721             }
722              
723             1;
724              
725              
726             __END__