From 1ccdd6ae136d7bf91ca3ef195b6ad4775e9fd010 Mon Sep 17 00:00:00 2001 From: Marcel Greter Date: Tue, 14 Nov 2017 18:55:42 +0100 Subject: [PATCH 1/2] Fix watching specific files on windows When you are only watching specific file paths, you get change events for all files in the same directory. On linux only changes to watched files get reported. --- lib/Filesys/Notify/Simple.pm | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/lib/Filesys/Notify/Simple.pm b/lib/Filesys/Notify/Simple.pm index c88016d..151d112 100644 --- a/lib/Filesys/Notify/Simple.pm +++ b/lib/Filesys/Notify/Simple.pm @@ -119,6 +119,8 @@ sub mk_wait_win32 { return sub { my @path = @_; + my %watched = map { ( $_ => 1 ) } + grep defined, map { Cwd::abs_path($_) } @path; my $fs = _full_scan(@path); my (@notify, @fskey); for my $path (keys %$fs) { @@ -133,11 +135,35 @@ sub mk_wait_win32 { my @events; while(1) { - my $idx = Win32::ChangeNotify::wait_any(\@notify); + my $idx = Win32::ChangeNotify::wait_any(\@notify); Carp::croak("Can't wait notifications, maybe ".scalar(@notify)." directories exceeds limitation.") if ! defined $idx; if($idx > 0) { --$idx; + # get all file changes in path my $new_fs = _full_scan($fskey[$idx]); + foreach my $root (keys %{$new_fs}) { + # on windows we can only watch folders + # therefore we need to filter unwanted + # events for files we are not looking for + unless (exists $watched{$root}) { + # process all reported file changes in path + foreach my $file (keys %{$new_fs->{$root}}) { + # don't remove if we watch particular file + unless (exists $fs->{$root}->{$file}) { + # we are not interested in this event + delete $new_fs->{$root}->{$file}; + } + } + } + # but only if we don't watch folder itself + else { + # check if we watch particular root + unless (exists $fs->{$root}) { + # remove paths not watched by us + delete $new_fs->{$root}; + } + } + } $notify[$idx]->reset; my $old_fs = +{ map { ($_ => $fs->{$_}) } keys %$new_fs }; _compare_fs($old_fs, $new_fs, sub { push @events, { path => $_[0] } }); @@ -210,7 +236,7 @@ sub _full_scan { # remove root entry # NOTE: On MSWin32, realpath and rel2abs disagree with path separator. - delete $map{$fp}{File::Spec->rel2abs($fp)}; + delete $map{$fp}{File::Spec->rel2abs($fp)} if exists $map{$fp}; } return \%map; From 08dfc236232ba1948e3f55e645e995707c178b6f Mon Sep 17 00:00:00 2001 From: Marcel Greter Date: Wed, 15 Nov 2017 03:20:45 +0100 Subject: [PATCH 2/2] Optimize windows functionality --- .gitignore | 2 + lib/Filesys/Notify/Simple.pm | 96 ++++++++++++++++++++++++++++-------- 2 files changed, 77 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 72c8833..d54e497 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ META.yml Makefile inc/ +blib/ +t/x/ pm_to_blib *~ MYMETA.* diff --git a/lib/Filesys/Notify/Simple.pm b/lib/Filesys/Notify/Simple.pm index 151d112..2a27581 100644 --- a/lib/Filesys/Notify/Simple.pm +++ b/lib/Filesys/Notify/Simple.pm @@ -119,14 +119,74 @@ sub mk_wait_win32 { return sub { my @path = @_; - my %watched = map { ( $_ => 1 ) } - grep defined, map { Cwd::abs_path($_) } @path; - my $fs = _full_scan(@path); + require File::Basename; + @path = map { Cwd::abs_path($_) } @path; + my (%observer, @dirs, @files, @roots); + + # split up watcher paths + foreach my $path (@path) { + if (-f $path) { push @files, $path; } + elsif (-d $path) { push @dirs, $path; } + } + + # get rid of inner paths + # no need to observer twice + foreach my $path (@dirs) { + my $hasroot = undef; + foreach my $root (@roots) { + # check if path starts with root (is subpath) + if (substr($path, 0, length($root)) eq $root) { + $hasroot = $root; + last; + } + } + push @roots, $path unless $hasroot; + } + + # observe directories recursively + foreach my $path (@dirs) { + if (exists $observer{$path}) { + $observer{$path}->{isdir} = 1; + } else { + $observer{$path} = { + files => [], + isdir => 1 + }; + } + } + + # observe files explicitly, but only if + # base directory is no watched recursively + foreach my $file (@files) { + my $path = File::Basename::dirname($file); + if (exists $observer{$path}) { + push @{$observer{$path}->{files}}, $path + } else { + $observer{$path} = { + files => [$path], + isdir => 0 + }; + } + } + + # optimize scan targets for observer + foreach my $path (keys %observer) { + $observer{$path}->{scan} = $observer{$path}->{isdir} + ? [ $path ] : \ @{$observer{$path}->{files}}; + } + + # either scan the whole directory or only the necessary files + my @scan = map { @{$observer{$_}->{scan}} } keys %observer; + + # get current filesystem state + my $fs = _full_scan(@scan); + my (@notify, @fskey); - for my $path (keys %$fs) { + + for my $path (keys %observer) { my $winpath = $is_cygwin ? Cygwin::posix_to_win_path($path) : $path; # 0x1b means 'DIR_NAME|FILE_NAME|LAST_WRITE|SIZE' = 2|1|0x10|8 - push @notify, Win32::ChangeNotify->new($winpath, 0, 0x1b); + push @notify, Win32::ChangeNotify->new($winpath, $observer{$path}->{isdir}, 0x1b); push @fskey, $path; } @@ -136,16 +196,18 @@ sub mk_wait_win32 { my @events; while(1) { my $idx = Win32::ChangeNotify::wait_any(\@notify); - Carp::croak("Can't wait notifications, maybe ".scalar(@notify)." directories exceeds limitation.") if ! defined $idx; + Carp::croak("Can't wait notifications, maybe " . scalar(@notify) . " directories exceeds limitation.") if ! defined $idx; if($idx > 0) { --$idx; - # get all file changes in path - my $new_fs = _full_scan($fskey[$idx]); - foreach my $root (keys %{$new_fs}) { - # on windows we can only watch folders - # therefore we need to filter unwanted - # events for files we are not looking for - unless (exists $watched{$root}) { + # get all file changes for observed path + my $observer = $observer{$fskey[$idx]}; + my $new_fs = _full_scan(@{$observer->{scan}}); + # on windows we can only watch folders + # therefore we need to filter unwanted + # events for files we are not looking for + # but only if we don't watch folder itself + unless ($observer->{isdir}) { + foreach my $root (keys %{$new_fs}) { # process all reported file changes in path foreach my $file (keys %{$new_fs->{$root}}) { # don't remove if we watch particular file @@ -155,14 +217,6 @@ sub mk_wait_win32 { } } } - # but only if we don't watch folder itself - else { - # check if we watch particular root - unless (exists $fs->{$root}) { - # remove paths not watched by us - delete $new_fs->{$root}; - } - } } $notify[$idx]->reset; my $old_fs = +{ map { ($_ => $fs->{$_}) } keys %$new_fs };