# $Id$ package main; use strict; use warnings; use Data::Dumper; my $missingModule = ""; eval { use Linux::Inotify2; }; unless($@) { Log 4, "inotify - found Linux::Inotify2"; } else { Log 1, "inotify - unable to load Linux::Inotify2 module: $@. Please install with sudo apt-get install liblinux-inotify2-perl"; $missingModule = "Linux::Inotify2 "; } eval { use File::Find; }; unless($@) { Log 4, "inotify - found File::Find"; } else { Log 1, "inotify - unable to load File::Find module: $@. Please install it."; $missingModule = "File::Find "; } eval "use File::Find;1" or $missingModule .= "File::Find "; ####################### # Global variables my $version = "0.6.1"; our $inotify; our @watch; my %gets = ( "version:noArg" => "", ); my @maskAttrs = ( "IN_ACCESS", "IN_MODIFY", "IN_ATTRIB", "IN_CLOSE_WRITE", "IN_CLOSE_NOWRITE", "IN_OPEN", "IN_MOVED_FROM", "IN_MOVED_TO", "IN_CREATE", "IN_DELETE", "IN_DELETE_SELF", "IN_MOVE_SELF", "IN_ALL_EVENTS", "IN_ONLYDIR", "IN_CLOSE", "IN_MOVE" ); sub inotify_Initialize($) { my ($hash) = @_; $hash->{SetFn} = "inotify_Set"; $hash->{GetFn} = "inotify_Get"; $hash->{DefFn} = "inotify_Define"; $hash->{UndefFn} = "inotify_Undefine"; $hash->{AttrFn} = "inotify_Attr"; $hash->{NotifyFn} = "inotify_Notify"; $hash->{ReadFn} = "inotify_Read"; $hash->{AttrList} = "disable:1,0 ". "do_not_notify ". "subfolders:1,0 ". "mask:multiple-strict,".join(',',@maskAttrs)." ". $readingFnAttributes; $hash->{NotifyOrderPrefix} = "81-"; ## renew version in reload foreach my $d ( sort keys %{ $modules{inotify}{defptr} } ) { my $hash = $modules{inotify}{defptr}{$d}; $hash->{VERSION} = $version; } return undef; } sub inotify_Define($$) { my ($hash, $def) = @_; my $now = time(); my $name = $hash->{NAME}; my @a = split( "[ \t][ \t]*", $def ); if ( int(@a) < 2 ) { my $msg = "Wrong syntax: define inotify []"; Log3 $name, 4, $msg; return $msg; } elsif ($a[3] && $a[3]=~/^\*.*/) { my $msg = "Wrong syntax: define inotify []. Please provide a valid regexp in ."; Log3 $name, 4, $msg; return $msg; } return "Cannot define a inotify device. Perl module(s) $missingModule is/are missing." if ( $missingModule ); $hash->{PATH}=$a[2]; $hash->{FILES}=$a[3]?$a[3]:undef; $hash->{VERSION} = $version; #$hash->{MID} = 'da39a3ee5e6dfdss434436657657bdbfef95601890afd80709'; # my $mid = "inotify_".$a[2].$a[3]; $mid =~ s/[^A-Za-z0-9\-_]//g; $hash->{MID} = $mid; $hash->{NOTIFYDEV}= "global"; $modules{inotify}{defptr}{ $hash->{MID} } = $hash; #MID for internal purposes ## start polling if ($init_done && !IsDisabled($name)) { readingsSingleUpdate($hash,"state","active",1); inotify_Watch($hash); } CommandAttr( undef, $name . ' mask IN_CREATE' ) if ( AttrVal( $name, 'mask', '-' ) eq '-' ); return undef; } sub inotify_Undefine($$) { my ($hash, $arg) = @_; RemoveInternalTimer($hash); return undef; } sub inotify_Notify ($$) { my ($hash,$dev) = @_; my $name = $hash->{NAME}; return if($dev->{NAME} ne "global"); return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}})); if (!IsDisabled($name)) { inotify_Watch($hash); $attr{$name}{"mask"}=~s/\|/,/g; } return undef; } sub inotify_Read($) { my ($hash) = @_; my $name = $hash->{NAME}; $inotify->poll; } sub inotify_Set ($@) { my ($hash, $name, $cmd, @args) = @_; my @sets = (); push @sets, "active:noArg" if (IsDisabled($name)); push @sets, "inactive:noArg" if (!IsDisabled($name)); return join(" ", @sets) if ($cmd eq "?"); my $usage = "Unknown argument ".$cmd.", choose one of ".join(" ", @sets) if(scalar @sets > 0); if (IsDisabled($name) && $cmd !~ /^(active|inactive)?$/) { Log3 $name, 3, "inotify ($name): Device is disabled at set Device $cmd"; return "Device is disabled. Enable it on order to use command ".$cmd; } if ( $cmd eq "inactive" ) { inotify_Inactive($hash); } elsif ( $cmd eq "active") { inotify_Active($hash); } else { return $usage; } return undef; } sub inotify_Get($@) { my ($hash, $name, $cmd, @args) = @_; my $ret = undef; if ( $cmd eq "version") { $hash->{VERSION} = $version; return "Version: ".$version; } else { $ret ="$name get with unknown argument $cmd, choose one of " . join(" ", sort keys %gets); } return $ret; } sub inotify_Attr($@) { my ($cmd, $name, $attrName, $attrVal) = @_; my $orig = $attrVal; my $hash = $defs{$name}; if ( $attrName eq "disable" ) { if ( $cmd eq "set" && $attrVal == 1 ) { if ($hash->{READINGS}{state}{VAL} ne "inactive") { inotify_Inactive($hash); } } elsif ( $cmd eq "del" || $attrVal == 0 ) { if ($hash->{READINGS}{state}{VAL} ne "active") { inotify_Active($hash,1); } } } if ( $attrName eq "mask" ) { if ( $cmd eq "set" ) { my @maskSet = split(',',$attrVal); my $check = 1; foreach (@maskSet) { if (!inotify_inArray(\@maskAttrs,$_)) { $check=0; } } return "$name: mask has to be a list of possible masks divided by comma. Select out of ".join(', ',@maskAttrs) if (!$check); Log3 $name, 4, "inotify ($name): set attribut $attrName to $attrVal"; inotify_setMasks ($hash,$attrVal); InternalTimer(gettimeofday()+1, "inotify_Watch", $hash, 0) if (!IsDisabled($name)); } elsif ( $cmd eq "del") { delete ($hash->{helper}{"masks"}); InternalTimer(gettimeofday()+1, "inotify_Watch", $hash, 0) if (!IsDisabled($name)); } } if ( $attrName eq "subfolders") { if ( $cmd eq "set" ) { return "$name: $attrName has to be 0 or 1" if ($attrVal !~ /^(0|1)$/); Log3 $name, 4, "inotify ($name): set attribut $attrName to $attrVal"; InternalTimer(gettimeofday()+1, "inotify_Watch", $hash, 0) if (!IsDisabled($name)); } elsif ( $cmd eq "del" ) { Log3 $name, 4, "inotify ($name): deleted attribut $attrName"; InternalTimer(gettimeofday()+1, "inotify_Watch", $hash, 0) if (!IsDisabled($name)); } } return; } sub inotify_Active($;$) { my ($hash,$ndel) = @_; my $name = $hash->{NAME}; $ndel = 0 if (!defined($ndel)); CommandDeleteAttr(undef,"$name disable") if (AttrVal($name,"disable",0)==1 && $ndel==0); InternalTimer(gettimeofday()+1, "inotify_Watch", $hash, 0); readingsSingleUpdate($hash,"state","active",1); Log3 $name, 3, "inotify ($name): set Device active"; return; } sub inotify_Inactive($) { my ($hash) = @_; my $name = $hash->{NAME}; readingsSingleUpdate($hash,"state","inactive",1); inotify_CancelWatches($hash); Log3 $name, 3, "inotify ($name): set Device inactive"; return; } sub inotify_Watch($) { my ($hash, $arg) = @_; my $path = $hash->{PATH}; my $name = $hash->{NAME}; my $subF = AttrVal($name,"subfolders",0); inotify_CancelWatches($hash,1); inotify_setMasks ($hash,AttrVal($name,"mask",undef)); $inotify = new Linux::Inotify2; $inotify->blocking(0); delete ($hash->{helper}{dirs}); if ($subF!=1) { push @{$hash->{helper}{dirs}},$path; $watch[0] = $inotify->watch ($path, IN_ALL_EVENTS, sub { my $e = shift; Log3 $name, 4, "inotify ($name): path to watch ".$path; inotify_AnalyseEvent($hash,$e); }); } else { my $i=0; my @dirs = split(/\n/,`find $path -type d`); @{$hash->{helper}{dirs}}=@dirs; foreach my $entry (@dirs) { Log3 $name, 4, "inotify ($name): path to watch ".$entry; $watch[$i] = $inotify->watch ($entry, IN_ALL_EVENTS, sub { my $e = shift; inotify_AnalyseEvent($hash,$e); }); $i++; } } $hash->{FD} = $inotify->fileno; $selectlist{$hash->{NAME}} = $hash; my $watchString = $path; if ($hash->{FILES} && $hash->{FILES} ne "") { $watchString .= " with the file pattern ".$hash->{FILES}; } Log3 $name, 3, "inotify ($name): startet watching ".$watchString; return; } sub inotify_CancelWatches($;$) { my ($hash,$noLog) = @_; $noLog = 0 if (!defined($noLog)); my $path = $hash->{PATH}; my $name = $hash->{NAME}; foreach my $w (@watch) { $w->cancel; } my $watchString = $path; if ($hash->{FILES} && $hash->{FILES} ne "") { $watchString .= " with the file pattern ".$hash->{FILES}; } delete($hash->{helper}{events}); Log3 $name, 3, "inotify ($name): stopped watching ".$watchString if (!$noLog); return; } sub inotify_AnalyseEvent($$) { my ($hash,$e) = @_; my $name = $hash->{NAME}; my $mask = "NA"; Log3 $name, 5, "inotify ($name): Fullname ".$e->fullname; if (($hash->{FILES} && $hash->{FILES}!~/^\*.*/ && $e->fullname=~/$hash->{FILES}/) || !$hash->{FILES}) { Log3 $name, 5, "inotify ($name): got ".Dumper($e); $mask="IN_ACCESS" if ($e->IN_ACCESS); $mask="IN_MODIFY" if ($e->IN_MODIFY); $mask="IN_ATTRIB" if ($e->IN_ATTRIB); $mask="IN_CLOSE_WRITE" if ($e->IN_CLOSE_WRITE); $mask="IN_CLOSE_NOWRITE" if ($e->IN_CLOSE_NOWRITE); $mask="IN_OPEN" if ($e->IN_OPEN); $mask="IN_MOVED_FROM" if ($e->IN_MOVED_FROM); $mask="IN_MOVED_TO" if ($e->IN_MOVED_TO); $mask="IN_CREATE" if ($e->IN_CREATE); $mask="IN_DELETE" if ($e->IN_DELETE); $mask="IN_DELETE_SELF" if ($e->IN_DELETE_SELF); $mask="IN_MOVE_SELF" if ($e->IN_MOVE_SELF); $mask="IN_Q_OVERFLOW" if ($e->IN_Q_OVERFLOW); if (!$hash->{helper}{"masks"} || inotify_inArray(\@{$hash->{helper}{"masks"}},$mask) || inotify_inArray(\@{$hash->{helper}{"masks"}},"IN_ALL_EVENTS")) { my $r=0; for (my $i=9;$i>=1;$i--) { $r=$i-1; $hash->{helper}{events}{$i}{"mask"}=$hash->{helper}{events}{$r}{"mask"} if ($hash->{helper}{events}{$r}{"mask"}); $hash->{helper}{events}{$i}{"file"}=$hash->{helper}{events}{$r}{"file"} if ($hash->{helper}{events}{$r}{"file"}); $hash->{helper}{events}{$i}{"time"}=$hash->{helper}{events}{$r}{"time"} if ($hash->{helper}{events}{$r}{"time"}); } $hash->{helper}{events}{0}{"mask"}=$mask; $hash->{helper}{events}{0}{"file"}=$e->fullname; $hash->{helper}{events}{0}{"time"}=TimeNow(); readingsBeginUpdate($hash); readingsBulkUpdate($hash,"lastEventFile",$e->fullname); readingsBulkUpdate($hash,"lastEventMask",$mask); readingsEndUpdate( $hash, 1 ); Log3 $name, 4, "inotify ($name): got event ".$mask." for ".$e->fullname; Log3 $name, 1, "inotify ($name): got error ".$mask." for ".$e->fullname if ($mask eq "IN_Q_OVERFLOW"); } else { Log3 $name, 4, "inotify ($name): event is not matching any configured mask: ".$mask; } my $subF = AttrVal($name,"subfolders",0); inotify_Watch($hash) if ($subF==1 && ((-d $e->fullname && $mask eq "IN_CREATE") || $mask eq "IN_DELETE")); } return; } sub inotify_setMasks ($$) { my ($hash,$attrVal) = @_; my $name = $hash->{NAME}; $attr{$name}{"mask"}=~s/\|/,/g; if ($attrVal) { my @masks = split(/\,/,$attrVal); @{$hash->{helper}{"masks"}} = @masks; } return undef; } sub inotify_inArray { my ($arr,$search_for) = @_; foreach (@$arr) { return 1 if ($_ eq $search_for); } return 0; } 1; =pod =item helper =item summary uses inotify to track on file events in a given path =item summary_DE überwacht gegebenen Ordner auf zu konfigurierende events =begin html

inotify

    This module collects file events in a given path. Inotify (inode notify) is a Linux kernel subsystem that
    acts to extend filesystems to notice changes to the filesystem, and report those changes to applications

    Notes:
    • Perl modules Data::Dumper, Linux::Inotify2, File::Find have to be installed on the FHEM host.

    Define

      define <name> inotify <path> [<file-RegEx>]

      • path: absolute path to watch
      • file-RegEx: file-Pattern (only watch a group of files)


      Example:
        define watchPath inotify /temp testfile.*

    Set

    • active - set active - watching starts
    • inactive - set iactive - watching stops

    Attributes

    Readings

    • lastEventFile
      the last modified file
    • lastEventMask
      the last mask we got in the event
    • state
      device is active or inactive
=end html =cut