File Coverage

blib/lib/POEx/DirNotify.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::DirNotify;
4              
5 1     1   39834 use 5.008008;
  1         4  
  1         64  
6 1     1   7 use strict;
  1         1  
  1         42  
7 1     1   5 use warnings;
  1         7  
  1         76  
8              
9             our $VERSION = '0.02_00';
10             $VERSION = eval $VERSION; # see L
11              
12 1     1   410 use POE;
  0            
  0            
13             use POE::Session::PlainCall;
14              
15             use Digest::MurmurHash qw( murmur_hash );
16             use Storable qw( dclone );
17             use POSIX qw( sigaction );
18             use Fcntl qw/F_NOTIFY O_RDONLY
19             DN_CREATE DN_ACCESS DN_MODIFY DN_RENAME
20             DN_DELETE DN_ATTRIB DN_MULTISHOT
21             /;
22              
23             sub DEBUG () { 0 }
24              
25             #############################################
26             sub spawn
27             {
28             my( $package, %init ) = @_;
29              
30             my $options = delete $init{options};
31             $options ||= {};
32              
33             POE::Session::PlainCall->create(
34             package => $package,
35             ctor_args => [ \%init ],
36             options => $options,
37             states => [ qw( _start _stop shutdown
38             monitor unmonitor
39             sig_notify
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} || 'dnotify';
54             return $self;
55             }
56              
57              
58             #############################################
59             sub _start
60             {
61             my( $self ) = @_;
62             poe->kernel->alias_set( $self->{alias} );
63             poe->kernel->sig( IO => 'sig_notify' );
64             poe->kernel->sig( shutdown => 'shutdown' );
65             }
66              
67             #############################################
68             sub _stop
69             {
70             }
71              
72              
73             #############################################
74             sub sig_notify
75             {
76             my( $self, $signame ) = @_;
77             DEBUG and warn "Notify $signame";
78             poe->kernel->sig_handled;
79             foreach my $event ( $self->_find_changes ) {
80             DEBUG and warn "Call @$event";
81             poe->kernel->post( @$event );
82             }
83             }
84              
85             #############################################
86             sub _find_path
87             {
88             my( $self, $path ) = @_;
89             return $self->{path}{ $path };
90             }
91              
92              
93             sub _build_details
94             {
95             my( $self, $path ) = @_;
96             my @stat = stat $path;
97             return 0 unless @stat;
98             $stat[11] = 0;
99             $stat[12] = 0;
100             return murmur_hash( join '-', @stat );
101             }
102              
103             #############################################
104             sub _find_changes
105             {
106             my( $self ) = @_;
107             my @ret;
108             foreach my $notify ( values %{ $self->{path} } ) {
109             my $details = $self->_build_details( $notify->{path} );
110             my $op = 'change';
111             $op = 'delete' unless $details;
112             if( $details ne $notify->{details} ) {
113             foreach my $event ( @{ $notify->{call} } ) {
114             my $E = dclone $event;
115             $E->[2] = { op=>$op,
116             path=>$notify->{path}
117             };
118             push @ret, $E;
119             }
120             }
121             }
122             return @ret;
123             }
124              
125             #############################################
126             sub monitor
127             {
128             my( $self, $args ) = @_;
129             my $path = $args->{path};
130             my $caller = join ' ', at => poe->caller_file,
131             line => poe->caller_line;
132              
133             my $flags = DN_MODIFY|DN_CREATE|DN_RENAME|DN_DELETE|DN_ATTRIB|DN_MULTISHOT; # XXX
134              
135             my $call = $self->_build_call( $args );
136              
137             my $notify = $self->_find_path( $path );
138             if( $notify ) {
139             DEBUG and warn "Monitor $path again\n";
140             push @{ $notify->{call} }, $call;
141             poe->kernel->refcount_increment( poe->sender, "NOTIFY $path" );
142             return 1;
143             }
144              
145              
146             unless( -d $path ) {
147             die "$path isn't a directory $caller\n";
148             }
149              
150             DEBUG and warn "Monitor $path\n";
151              
152             my $details = $self->_build_details( $path );
153              
154             $notify = {
155             path => $path,
156             call => [ $call ],
157             details => $details,
158             fd => undef,
159             };
160              
161             $self->{path}{$path} = $notify;
162              
163             my $fd;
164             sysopen( $fd, $path, O_RDONLY ) or die "sysopen $path: $! $caller\n";
165             fcntl( $fd, F_NOTIFY, $flags ) or warn "fcntl F_NOTIFY: $!";
166              
167             $notify->{fd} = $fd;
168             poe->kernel->refcount_increment( poe->session->ID, "NOTIFY $path" );
169             poe->kernel->refcount_increment( poe->sender, "NOTIFY $path" );
170              
171             return 1;
172             }
173              
174             sub _build_call
175             {
176             my( $self, $args ) = @_;
177             my $event = $args->{event};
178             my $A = $args->{args};
179             my $session = poe->sender;
180              
181             my $call = [ $session, $event, undef ];
182             if( $A ) {
183             $A = dclone $A if ref $A;
184             if( 'ARRAY' eq ref $A ) {
185             push @$call, @$A;
186             }
187             else {
188             push @$call, $A;
189             }
190             }
191             return $call;
192             }
193              
194              
195             #############################################
196             sub unmonitor
197             {
198             my( $self, $args ) = @_;
199             my $path = $args->{path};
200             my $session = poe->sender;
201             my $caller = join ' ', at => poe->caller_file,
202             line => poe->caller_line;
203             my $notify = $self->_find_path( $path );
204             unless( $notify ) {
205             warn "$path wasn't monitored $caller\n";
206             return;
207             }
208              
209             my @calls;
210             foreach my $call ( @{ $notify->{call} } ) {
211             if( $call->[0] eq $session or $self->{force} ) {
212             poe->kernel->refcount_decrement( $session, "NOTIFY $notify->{path}" );
213             }
214             else {
215             push @calls, $call;
216             }
217             }
218              
219             if( @calls ) {
220             $notify->{call} = \@calls;
221             DEBUG and warn "$path still being monitored\n";
222             return;
223             }
224             DEBUG and warn "Unmonitor $path\n";
225              
226             fcntl( $notify->{fd}, F_NOTIFY, 0 ) or warn "fcntl F_NOTIFY: $! $caller\n";
227              
228             poe->kernel->refcount_decrement( poe->session->ID, "NOTIFY $notify->{path}" );
229             delete $notify->{fd};
230             delete $self->{path}{ $path };
231             return;
232             }
233              
234             #############################################
235             sub shutdown
236             {
237             my( $self ) = @_;
238             DEBUG and warn "Shutdown $self->{alias}\n";
239             foreach my $path ( keys %{ $self->{path} } ) {
240             local $self->{force} = 1;
241             $self->unmonitor( { path=>$path } );
242             }
243             }
244              
245              
246              
247              
248             1;
249             __END__