unit dirwatch; {$IFDEF LINUX} {$DEFINE USEINOTIFY} {$ELSE} {$DEFINE USEGENERIC} {$ENDIF} {$mode objfpc}{$H+} interface uses Classes, SysUtils, {$IFDEF UNIX} baseunix, {$IFDEF USEINOTIFY} ctypes, linux, {$ENDIF} {$ENDIF} contnrs; Type TFileEvent = (feModify,feAttrib,feCreate,feDelete); TFileEvents = set of TFileEvent; { TDirectoryEntry } TDirectoryEntry = Class(TCollectionItem) private FEvents: TFileEvents; FName: String; FAttributes: Integer; FGroup: gid_t; FMode: mode_t; FOwner: uid_t; FSize: Int64; FTimeStamp: TDateTime; Protected {$IFDEF USEGENERIC} procedure InitWatch(ABaseDir: String; AList: TFPStringHashTable); {$ENDIF} Public Property TimeStamp : TDateTime Read FTimeStamp Write FTimeStamp; Property Size : Int64 Read FSize Write FSize; Property Attributes : Integer Read FAttributes Write FAttributes; {$IFDEF UNIX} Property Mode : mode_t Read FMode Write FMode; Property Owner : uid_t Read FOwner Write FOwner; Property Group : gid_t Read FGroup Write FGroup; {$ENDIF} Published Property Name : String Read FName Write FName; Property Events : TFileEvents Read FEvents Write FEvents; end; { TDirectoryEntries } TDirectoryEntries = Class(TCollection) private function GetE(AIndex : Integer): TDirectoryEntry; procedure SetE(AIndex : Integer; AValue: TDirectoryEntry); Public Function IndexOfEntry(Const AName : String) : Integer; Function EntryByName(Const AName : String) : TDirectoryEntry; Function AddEntry(Const AName : String) : TDirectoryEntry; Property Entries[AIndex : Integer] : TDirectoryEntry Read GetE Write SetE; default; end; TFileEventHandler = procedure (Sender : TObject; aEntry : TDirectoryEntry; AEvents : TFileEvents) of Object; { TDirwatch } TDirwatch = Class(TComponent) private FIdleInterval: Cardinal; FOnIdle: TNotifyEvent; FOnIdleNotify: TNotifyEvent; FTerminated: Boolean; FThreaded: Boolean; FWatches: TDirectoryEntries; FBaseDir: String; FOnChange: TFileEventHandler; {$IFDEF USEGENERIC} FReference : TFPStringHashTable; FOldReference : TFPStringHashTable; procedure DoCheckItem(Item: String; const Key: string; var Continue: Boolean); procedure DoDeletedItem(Item: String; const Key: string; var Continue: Boolean); {$ENDIF} {$IFDEF USEINOTIFY} FINotifyFD : Cint; {$ENDIF} function DirectoryEntryForFileName(S: String): TDirectoryEntry; procedure DoChangeEvent(Entry: TDirectoryEntry; Events: TFileEvents); procedure SetBaseDir(AValue: String); Protected procedure DoIdle; virtual; procedure Check; virtual; procedure DoneWatch; virtual; procedure DoStartWatch; virtual; procedure InitWatch;virtual; Public Constructor Create(AOWner : TComponent); override; Destructor Destroy; override; Procedure StartWatch; Procedure AddWatch(const aFileName : string; aEvents : TFileEvents); Procedure Terminate; Property Terminated : Boolean Read FTerminated; Published Property BaseDir : String read FBaseDir Write SetBaseDir; Property OnChange : TFileEventHandler Read FOnChange Write FOnChange; Property Threaded : Boolean Read FThreaded Write FThreaded; Property Watches : TDirectoryEntries Read FWatches Write FWatches; Property OnIdle : TNotifyEvent Read FOnIdle Write FOnIdleNotify; Property IdleInterval : Cardinal Read FIdleInterval Write FIdleInterval; end; Const EventNames : Array[TFileEvent] of string = ('Modify','Attrib','Create','Delete'); AllEvents = [feModify,feAttrib,feCreate,feDelete]; Function FileEventsToStr(Events : TFileEvents) : String; implementation Function FileEventsToStr(Events : TFileEvents) : String; Var E : TFileEvent; begin Result:=''; for E in Events do begin if Result<>'' then Result:=Result+','; Result:=Result+EventNames[E]; end; end; { TDirwatch } Type { TDirwatchThread } TDirwatchThread = class(TThread) Private FDir:TDirWatch; Public Constructor Create(ADirwatch : TDirWatch); Procedure Execute; override; end; { TDirectoryEntry } Function SearchRecToString(Info : TSearchRec; AEvents : TFileEvents) : String; begin if feAttrib in AEvents then Result:=IntToStr(Info.Attr) else Result:=''; Result:=Result+';'+IntToStr(Info.Size)+';'+IntToStr(Info.Time); end; {$IFDEF USEGENERIC} procedure TDirectoryEntry.InitWatch(ABaseDir: String; AList: TFPStringHashTable); Var Info : TSearchRec; FN : String; begin if (ABaseDir<>'') then FN:=IncludeTrailingPathDelimiter(ABaseDir)+Name else FN:=Name; if FindFirst(FN,faAnyFile,Info)=0 then begin if (faDirectory and Info.Attr) = 0 then begin AList.Add(FN,SearchRecToString(Info,Self.Events)) end else begin FindClose(Info); FN:=IncludeTrailingPathDelimiter(FN); if FindFirst(FN+AllFilesMask,0,Info)=0 then Repeat if (info.Name<>'.') and (Info.Name<>'..') then AList.Add(FN+Info.Name,SearchRecToString(Info,Self.Events)); until (FindNext(Info)<>0) end; FindClose(Info); end end; {$ENDIF} {$IFDEF USEINOTIFY} {$ENDIF} { TDirwatchThread } constructor TDirwatchThread.Create(ADirwatch: TDirWatch); begin FDir:=ADirWatch; FreeOnTerminate:=True; inherited create(False); end; procedure TDirwatchThread.Execute; begin FDir.DoStartWatch; end; procedure TDirwatch.SetBaseDir(AValue: String); begin if FBaseDir=AValue then Exit; FBaseDir:=AValue; FWatches.Clear; end; constructor TDirwatch.Create(AOWner: TComponent); begin inherited Create(AOWner); FWatches:=TDirectoryEntries.Create(TDirectoryEntry); FidleInterval:=100; end; destructor TDirwatch.Destroy; begin FreeAndNil(FWatches); inherited Destroy; end; Type { TDirwatchChange } TDirwatchChange = Class FEntry : TDirectoryEntry; FEvents : TFileEvents; FDirWatch : TDirWatch; Constructor Create(AEntry : TDirectoryEntry;aEvents : TFileEvents;ADirWatch : TDirWatch); Procedure DoEvent; end; { TDirwatchChange } constructor TDirwatchChange.Create(AEntry: TDirectoryEntry; aEvents: TFileEvents; ADirWatch: TDirWatch); begin FEntry:=AEntry; FEvents:=AEvents; FDirWatch:=ADirWatch; end; procedure TDirwatchChange.DoEvent; begin FDirwatch.FonChange(FDirwatch,FEntry,FEvents); end; Procedure TDirwatch.DoChangeEvent(Entry : TDirectoryEntry; Events : TFileEvents); Var W : TDirWatchChange; begin try if Assigned(FOnChange) then if Not Threaded then FonChange(Self,Entry,Events) else begin W:=TDirWatchChange.Create(Entry,Events,Self); try TThread.Synchronize(TThread.CurrentThread,@W.DoEvent) finally W.Free; end; end Finally // Specially created if Entry.Collection=Nil then FreeAndNil(Entry); end; end; procedure TDirwatch.DoIdle; begin if Assigned(FOnIdle) then FOnIdle(Self); end; Function TDirwatch.DirectoryEntryForFileName(S : String) : TDirectoryEntry; begin Result:=FWatches.EntryByName(S); if (Result=Nil) then Result:=FWatches.EntryByName(ExtractFilePath(S)); if (Result=Nil) then begin Result:=TDirectoryEntry.Create(Nil); Result.Name:=S; end; end; {$IFDEF USEGENERIC} procedure TDirwatch.DoneWatch; begin FreeAndNil(FReference); end; procedure TDirwatch.InitWatch; Var I : Integer; begin FReference:=TFPStringHashTable.Create; For I:=0 to FWatches.Count-1 do FWatches[i].InitWatch(BaseDir,FReference); end; procedure TDirwatch.DoDeletedItem(Item: String; const Key: string; var Continue: Boolean); Var DE : TDirectoryEntry; begin DE:=FWatches.EntryByName(Key); if (DE=Nil) then DE:=FWatches.EntryByName(ExtractFilePath(Key)); if (DE=Nil) then begin DE:=TDirectoryEntry.Create(Nil); DE.Name:=Key; end; DoChangeEvent(DE,[feDelete]); Continue:=False; end; procedure TDirwatch.DoCheckItem(Item: String; const Key: string; var Continue: Boolean); Var S : String; E : TFileEvents; DE : TDirectoryEntry; begin // Writeln('check file: ',key,' attrs : ',Item); E:=[]; S:=FOldReference[Key]; if (S='') then E:=[feCreate] else begin FOldReference.Delete(Key); if (S<>Item) then E:=[feAttrib]; end; if E<>[] then begin DE:=DirectoryEntryForFileName(Key); DoChangeEvent(DE,E); Continue:=False; end; end; procedure TDirwatch.Check; begin FOldReference:=FReference; try FReference:=TFPStringHashTable.Create; InitWatch; FReference.Iterate(@doCheckItem); if FoldReference.Count>0 then FReference.Iterate(@doDeletedItem); // Deleted files Sleep(IdleInterval); finally FreeAndNil(FoldReference); end; end; {$ENDIF} {$IFDEF USEINOTIFY} Procedure WatchDirectory(d : string); Const Events = IN_MODIFY or IN_ATTRIB or IN_CREATE or IN_DELETE; Var fd, wd,fnl,len : cint; fds : tfdset; e : ^inotify_event; buf : Array[0..1023*4] of Byte; // 4K Buffer fn : string; p : pchar; begin fd:=inotify_init; try wd:=inotify_add_watch(fd,pchar(d),Events); fpFD_Zero(fds); fpFD_SET(fd,fds); While (fpSelect(fd+1,@fds,nil,nil,nil)>=0) do begin len:=fpRead(fd,buf,sizeof(buf)); e:=@buf; While ((pchar(e)-@buf)0) then begin p:=@e^.name+fnl-1; While (p^=#0) do begin dec(p); dec(fnl); end; end; setlength(fn,fnl); if (fnl>0) then move(e^.name,fn[1],fnl); Writeln('Change ',e^.mask,' (', // InotifyEventsToString(e^.mask), ') detected for file "',fn,'"'); ptrint(e):=ptrint(e)+sizeof(inotify_event)+e^.len-1; end; end; finally fpClose(fd); end; end; procedure TDirwatch.DoneWatch; begin fpClose(FInotifyFD); end; procedure TDirwatch.InitWatch; Const NativeEvents : Array[TFileEvent] of cint = (IN_Modify,IN_Attrib,IN_Create,IN_Delete); Var WD,I,NEvents : Integer; E : TFileEvent; BD,FN : String; begin BD:=BaseDir; if BD<>'' then BD:=IncludeTrailingPathDelimiter(BD); FINotifyFD:=inotify_init; For I:=0 to FWatches.Count-1 do begin NEvents:=0; for E in FWatches[i].Events do NEvents:=NEvents OR NativeEvents[E]; FN:=BD+FWatches[i].Name; wd:=inotify_add_watch(FINotifyFD,PChar(FN),NEvents); end; end; Function NativeEventsToEvents(Native : cint) : TFileEvents; Procedure MA(C : cint; AEvent : TFileEvent); begin if (Native and C)<>0 then Include(Result,AEvent); end; begin Result:=[]; MA(IN_ACCESS,feAttrib); MA(IN_MODIFY,feModify); MA(IN_ATTRIB,feAttrib); MA(IN_CLOSE_WRITE,feAttrib); MA(IN_CLOSE_NOWRITE,feAttrib); MA(IN_OPEN,feAttrib); MA(IN_MOVED_FROM,feCreate); MA(IN_MOVED_TO,feDelete); MA(IN_CREATE,feCreate); Ma(IN_DELETE,feDelete); Ma(IN_DELETE_SELF,feDelete); Ma(IN_MOVE_SELF,feDelete); Ma(IN_UNMOUNT,feDelete); // IN_Q_OVERFLOW // IN_IGNORED end; procedure TDirwatch.Check; Var fnl,len : cint; e : ^inotify_event; buf : Array[0..1023*4] of Byte; // 4K Buffer fn : string; p : pchar; fds : tfdset; Timeout : ttimeval; begin fpFD_Zero(fds); fpFD_SET(FINotifyFD,fds); timeout.tv_sec:=FIdleInterval div 1000; timeout.tv_usec:=(FIdleInterval mod 1000)*1000; if (fpSelect(FINotifyFD+1,@fds,nil,nil,@Timeout)<=0) then exit; len:=fpRead(FINotifyFD,buf,sizeof(buf)); e:=@buf; While ((pchar(e)-@buf)0) then begin p:=@e^.name+fnl-1; While (p^=#0) do begin dec(p); dec(fnl); end; end; setlength(fn,fnl); if (fnl>0) then move(e^.name,fn[1],fnl); DoChangeEvent(DirectoryEntryForFileName(FN),NativeEventsToEvents(E^ .mask)); ptrint(e):=ptrint(e)+sizeof(inotify_event)+e^.len-1; end; end; {$ENDIF} procedure TDirwatch.DoStartWatch; begin InitWatch; try While not Terminated do begin Check; if Threaded then TThread.Synchronize(TThread.CurrentThread,@DoIdle) else DoIdle; end; Finally DoneWatch; end; end; procedure TDirwatch.StartWatch; begin If Threaded then TDirwatchThread.Create(Self).WaitFor else DoStartWatch; end; procedure TDirwatch.AddWatch(const aFileName: string; aEvents: TFileEvents); begin FWatches.AddEntry(AFileName).Events:=AEvents; end; procedure TDirwatch.Terminate; begin FTerminated:=True; end; { TDirectoryEntries } function TDirectoryEntries.GetE(AIndex : Integer): TDirectoryEntry; begin Result:=TDirectoryEntry(Items[AIndex]); end; procedure TDirectoryEntries.SetE(AIndex : Integer; AValue: TDirectoryEntry); begin Items[AIndex]:=AValue; end; function TDirectoryEntries.IndexOfEntry(const AName: String): Integer; begin Result:=Count-1; While (Result>=0) and (GetE(Result).Name<>AName) do Dec(Result); end; function TDirectoryEntries.EntryByName(const AName: String): TDirectoryEntry; Var I : Integer; begin I:=IndexOfEntry(AName); If (I=-1) then Result:=Nil else Result:=GetE(I); end; function TDirectoryEntries.AddEntry(Const AName: String): TDirectoryEntry; begin Result:=Add as TDirectoryEntry; Result.Name:=AName; end; end.