pas2js/demo/hotreload/dirwatch.pp
2017-12-16 14:45:10 +00:00

623 lines
13 KiB
ObjectPascal

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)<len) do
begin
fnl:=e^.len;
if (fnl>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)<len) do
begin
fnl:=e^.len;
if (fnl>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.