File Coverage

blib/lib/Filesys/Notify/Simple.pm
Criterion Covered Total %
statement 66 151 43.7
branch 13 32 40.6
condition 18 40 45.0
subroutine 14 22 63.6
pod 0 8 0.0
total 111 253 43.8


line stmt bran cond sub pod time code
1             package Filesys::Notify::Simple;
2              
3 6     6   70908 use strict;
  6         35  
  6         161  
4 6     6   123 use 5.008_001;
  6         22  
5             our $VERSION = '0.14';
6              
7 6     6   32 use Carp ();
  6         11  
  6         151  
8 6     6   29 use Cwd;
  6         10  
  6         515  
9 6     6   39 use constant NO_OPT => $ENV{PERL_FNS_NO_OPT};
  6         10  
  6         11552  
10              
11             sub new {
12 5     5 0 5607 my($class, $path) = @_;
13              
14 5 50       26 unless (ref $path eq 'ARRAY') {
15 0         0 Carp::croak('Usage: Filesys::Notify::Simple->new([ $path1, $path2 ])');
16             }
17              
18 5         21 my $self = bless { paths => $path }, $class;
19 5         20 $self->init;
20              
21 5         24 $self;
22             }
23              
24             sub wait {
25 5     5 0 6897 my($self, $cb) = @_;
26              
27 5   66     156 $self->{watcher} ||= $self->{watcher_cb}->(@{$self->{paths}});
  3         63  
28 5         27 $self->{watcher}->($cb);
29             }
30              
31             sub init {
32 5     5 0 12 my $self = shift;
33              
34 5         12 local $@;
35 5 50 50     66 if ($^O eq 'linux' && !NO_OPT && eval { require Linux::Inotify2; 1 }) {
  4 50 66     790  
  0 50 50     0  
    50 33        
    50 33        
      50        
      33        
      50        
      33        
      50        
      33        
36 0         0 $self->{watcher_cb} = \&wait_inotify2;
37 0         0 } elsif ($^O eq 'darwin' && !NO_OPT && eval { require Mac::FSEvents; 1 }) {
  0         0  
38 0         0 $self->{watcher_cb} = \&wait_fsevents;
39 0         0 } elsif (($^O eq 'freebsd' || $^O eq 'openbsd') && !NO_OPT && eval { require Filesys::Notify::KQueue; 1 }) {
  0         0  
40 0         0 $self->{watcher_cb} = \&wait_kqueue;
41 0         0 } elsif ($^O eq 'MSWin32' && !NO_OPT && eval { require Win32::ChangeNotify; 1 }) {
  0         0  
42 0         0 $self->{watcher_cb} = mk_wait_win32(0); # Not cygwin
43 0         0 } elsif ($^O eq 'cygwin' && !NO_OPT && eval { require Win32::ChangeNotify; 1 }) {
  0         0  
44 0         0 $self->{watcher_cb} = mk_wait_win32(1); # Cygwin
45             } else {
46 5         40 $self->{watcher_cb} = \&wait_timer;
47             }
48             }
49              
50             sub wait_inotify2 {
51 0     0 0 0 my @path = @_;
52              
53 0         0 Linux::Inotify2->import;
54 0         0 my $inotify = Linux::Inotify2->new;
55              
56 0         0 my $fs = _full_scan(@path);
57 0         0 for my $path (keys %$fs) {
58 0 0       0 $inotify->watch($path, &IN_MODIFY|&IN_CREATE|&IN_DELETE|&IN_DELETE_SELF|&IN_MOVE_SELF|&IN_MOVE)
59             or Carp::carp("watch failed for $path: $!");
60             }
61              
62             return sub {
63 0     0   0 my $cb = shift;
64 0         0 $inotify->blocking(1);
65 0         0 my @events = $inotify->read;
66 0         0 $cb->(map { +{ path => $_->fullname } } @events);
  0         0  
67 0         0 };
68             }
69              
70             sub wait_fsevents {
71 0     0 0 0 require IO::Select;
72 0         0 my @path = @_;
73              
74 0         0 my $fs = _full_scan(@path);
75 0         0 my $sel = IO::Select->new;
76              
77 0         0 my %events;
78 0         0 for my $path (@path) {
79 0         0 my $fsevents = Mac::FSEvents->new({ path => $path, latency => 1, file_events => 1 });
80 0         0 my $fh = $fsevents->watch;
81 0         0 $sel->add($fh);
82 0         0 $events{fileno $fh} = $fsevents;
83             }
84              
85             return sub {
86 0     0   0 my $cb = shift;
87              
88 0         0 my @ready = $sel->can_read;
89 0         0 my @events;
90 0         0 for my $fh (@ready) {
91 0         0 my $fsevents = $events{fileno $fh};
92 0         0 my %uniq;
93 0         0 my @path = grep !$uniq{$_}++, map { $_->path } $fsevents->read_events;
  0         0  
94              
95 0         0 my $new_fs = _full_scan(@path);
96 0         0 my $old_fs = +{ map { ($_ => $fs->{$_}) } keys %$new_fs };
  0         0  
97 0         0 _compare_fs($old_fs, $new_fs, sub { push @events, { path => $_[0] } });
  0         0  
98 0         0 $fs->{$_} = $new_fs->{$_} for keys %$new_fs;
99 0 0       0 last if @events;
100             }
101              
102 0         0 $cb->(@events);
103 0         0 };
104             }
105              
106             sub wait_kqueue {
107 0     0 0 0 my @path = @_;
108              
109 0         0 my $kqueue = Filesys::Notify::KQueue->new(
110             path => \@path
111             );
112              
113 0     0   0 return sub { $kqueue->wait(shift) };
  0         0  
114             }
115              
116             sub mk_wait_win32 {
117 0     0 0 0 my ($is_cygwin) = @_;
118              
119             return sub {
120 0     0   0 my @path = @_;
121              
122 0         0 my $fs = _full_scan(@path);
123 0         0 my (@notify, @fskey);
124 0         0 for my $path (keys %$fs) {
125 0 0       0 my $winpath = $is_cygwin ? Cygwin::posix_to_win_path($path) : $path;
126             # 0x1b means 'DIR_NAME|FILE_NAME|LAST_WRITE|SIZE' = 2|1|0x10|8
127 0         0 push @notify, Win32::ChangeNotify->new($winpath, 0, 0x1b);
128 0         0 push @fskey, $path;
129             }
130              
131             return sub {
132 0         0 my $cb = shift;
133              
134 0         0 my @events;
135 0         0 while(1) {
136 0         0 my $idx = Win32::ChangeNotify::wait_any(\@notify);
137 0 0       0 Carp::croak("Can't wait notifications, maybe ".scalar(@notify)." directories exceeds limitation.") if ! defined $idx;
138 0 0       0 if($idx > 0) {
139 0         0 --$idx;
140 0         0 my $new_fs = _full_scan($fskey[$idx]);
141 0         0 $notify[$idx]->reset;
142 0         0 my $old_fs = +{ map { ($_ => $fs->{$_}) } keys %$new_fs };
  0         0  
143 0         0 _compare_fs($old_fs, $new_fs, sub { push @events, { path => $_[0] } });
  0         0  
144 0         0 $fs->{$_} = $new_fs->{$_} for keys %$new_fs;
145 0 0       0 last if @events; # Actually changed
146             }
147             }
148 0         0 $cb->(@events);
149             }
150 0         0 }
151 0         0 }
152              
153             sub wait_timer {
154 3     3 0 64 my @path = @_;
155              
156 3         61 my $fs = _full_scan(@path);
157              
158             return sub {
159 5     5   13 my $cb = shift;
160 5         8 my @events;
161 5         9 while (1) {
162 7         13000843 sleep 2;
163 7         158 my $new_fs = _full_scan(@path);
164 6         84 _compare_fs($fs, $new_fs, sub { push @events, { path => $_[0] } });
  5         19  
165 6         42 $fs = $new_fs;
166 6 100       23 last if @events;
167             };
168 4         23 $cb->(@events);
169 3         82 };
170             }
171              
172             sub _compare_fs {
173 6     6   21 my($old, $new, $cb) = @_;
174              
175 6         35 for my $dir (keys %$old) {
176 24         40 for my $path (keys %{$old->{$dir}}) {
  24         65  
177 20 100 33     159 if (!exists $new->{$dir}{$path}) {
    50 66        
178 2         8 $cb->($path); # deleted
179             } elsif (!$new->{$dir}{$path}{is_dir} &&
180             ( $old->{$dir}{$path}{mtime} != $new->{$dir}{$path}{mtime} ||
181             $old->{$dir}{$path}{size} != $new->{$dir}{$path}{size})) {
182 0         0 $cb->($path); # updated
183             }
184             }
185             }
186              
187 6         25 for my $dir (keys %$new) {
188 24         35 for my $path (sort grep { !exists $old->{$dir}{$_} } keys %{$new->{$dir}}) {
  21         72  
  24         58  
189 3         12 $cb->($path); # new
190             }
191             }
192             }
193              
194             sub _full_scan {
195 9     9   81 my @paths = @_;
196 9         187 require File::Find;
197              
198 9         30 my %map;
199 9         65 for my $path (@paths) {
200 17 100       215 my $fp = eval { Cwd::realpath($path) } or next;
  17         979  
201             File::Find::finddepth({
202             wanted => sub {
203 43   33 43   184 my $fullname = $File::Find::fullname || File::Spec->rel2abs($File::Find::name);
204 43         151 $map{Cwd::realpath($File::Find::dir)}{$fullname} = _stat($fullname);
205             },
206 16         5423 follow_fast => 1,
207             follow_skip => 2,
208             no_chdir => 1,
209             }, $path);
210              
211             # remove root entry
212             # NOTE: On MSWin32, realpath and rel2abs disagree with path separator.
213 16         642 delete $map{$fp}{File::Spec->rel2abs($fp)};
214             }
215              
216 9         52 return \%map;
217             }
218              
219             sub _stat {
220 43     43   82 my $path = shift;
221 43         529 my @stat = stat $path;
222 43         2229 return { path => $path, mtime => $stat[9], size => $stat[7], is_dir => -d _ };
223             }
224              
225              
226             1;
227             __END__