UPD: DARWIN: Renamed Event supported in fswatch on MacOS (#847)

This commit is contained in:
rich2014 2023-03-05 15:43:51 +08:00 committed by GitHub
commit 409cdae2bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 258 additions and 72 deletions

View file

@ -907,11 +907,18 @@ begin
([ecStructChanged, ecAttribChanged] * event.categories = [ecAttribChanged])
then exit;
FCurrentEventData.FileName := ExtractFileName( event.fullPath );
if TDarwinFSWatchEventCategory.ecRemoved in event.categories then
FCurrentEventData.EventType := fswFileDeleted
else if TDarwinFSWatchEventCategory.ecRenamed in event.categories then
FCurrentEventData.EventType := fswUnknownChange
else if TDarwinFSWatchEventCategory.ecCreated in event.categories then
else if TDarwinFSWatchEventCategory.ecRenamed in event.categories then begin
if ExtractFilePath(event.fullPath)=ExtractFilePath(event.renamedPath) then begin
// fswFileRenamed only when FileName and NewFileName in the same dir
// otherwise keep fswUnknownChange
FCurrentEventData.EventType := fswFileRenamed;
FCurrentEventData.NewFileName := ExtractFileName( event.renamedPath );
end;
end else if TDarwinFSWatchEventCategory.ecCreated in event.categories then
FCurrentEventData.EventType := fswFileCreated
else if TDarwinFSWatchEventCategory.ecCoreAttribChanged in event.categories then
FCurrentEventData.EventType := fswFileChanged
@ -926,7 +933,6 @@ begin
FCurrentEventData.OriginalEvent := nil;
end;
{$ENDIF}
{$IF DEFINED(HAIKUQT)}
@ -984,7 +990,6 @@ begin
{$IFDEF DARWIN}
// FlatView Watch is supported on MacOS
// FCurrentEventData.Path contains WatchPath
// FCurrentEventData.FileName contains FullPath
// so in FlatView Mode, Path need to be adjusted to the Real Path
if TFileView(UserData).FlatView then begin
FCurrentEventData.Path := ExcludeTrailingPathDelimiter(ExtractFilePath(FCurrentEventData.OriginalEvent.fullPath));
@ -994,7 +999,6 @@ begin
continue;
FCurrentEventData.Path := AWatchPath;
end;
FCurrentEventData.FileName := ExtractFileName( FCurrentEventData.OriginalEvent.fullPath );
{$ENDIF}
WatcherEvent(FCurrentEventData);
end;

View file

@ -23,7 +23,7 @@
1. multiple directories can be monitored at the same time.
DC generally monitors more than 2 directories (possibly much more than 2),
just one TDarwinFSWatcher needed.
2. subdirectories monitor supported. currently in DC, only one level needed.
2. subdirectories monitor supported.
3. file attributes monitoring is supported, and monitoring of adding files,
renaming files, deleting files is also supported.
for comparison, file attributes monitoring is missing with kqueue/kevent.
@ -40,8 +40,8 @@ unit uDarwinFSWatch;
interface
uses
Classes, SysUtils, SyncObjs,
MacOSAll, CocoaAll;
Classes, SysUtils, Contnrs, SyncObjs,
MacOSAll, CocoaAll, BaseUnix;
type TDarwinFSWatchEventCategory = (
ecAttribChanged, ecCoreAttribChanged, ecXattrChanged,
@ -51,16 +51,46 @@ type TDarwinFSWatchEventCategory = (
type TDarwinFSWatchEventCategories = set of TDarwinFSWatchEventCategory;
type TInternalEvent = class
private
path: String;
renamedPath: String; // only for Ranamed Event
flags: FSEventStreamEventFlags;
iNode: UInt64;
private
function deepCopy(): TInternalEvent;
end;
type TDarwinFSWatchEventSession = class
private
_list: TObjectList;
public
constructor create; overload;
constructor create( const amount:size_t; eventPaths:CFArrayRef; flags:FSEventStreamEventFlagsPtr );
destructor destroy; override;
private
function deepCopy(): TDarwinFSWatchEventSession;
function count(): Integer;
function getItem( index:Integer ): TInternalEvent;
procedure adjustRenamedEventIfNecessary( index:Integer );
function isRenamed( event:TInternalEvent ): Boolean;
property items[index: Integer]: TInternalEvent read getItem; default;
end;
type TDarwinFSWatchEvent = class
private
_categories: TDarwinFSWatchEventCategories;
_watchPath: String;
_fullPath: String;
_renamedPath: String;
_rawEventFlags: UInt32;
_rawINode: UInt64;
private
procedure createCategories;
{%H-}constructor create( const aWatchPath:String; const internalEvent:TInternalEvent );
public
constructor create( const aWatchPath:String; const aFullPath:String; const aFlags:UInt32 );
function isDropabled(): Boolean;
function categoriesToStr(): String;
function rawEventFlagsToStr(): String;
@ -68,7 +98,9 @@ public
property categories: TDarwinFSWatchEventCategories read _categories;
property watchPath: String read _watchPath;
property fullPath: String read _fullPath;
property renamedPath: String read _renamedPath;
property rawEventFlags: UInt32 read _rawEventFlags;
property rawINode: UInt64 read _rawINode;
end;
type TDarwinFSWatchCallBack = Procedure( event:TDarwinFSWatchEvent ) of object;
@ -92,8 +124,8 @@ private
_lockObject: TCriticalSection;
_pathsSyncObject: TEventObject;
private
procedure handleEvents( const amount:size_t; paths:PPChar; flags:FSEventStreamEventFlagsPtr; ids:FSEventStreamEventIdPtr );
procedure doCallback( const path:String; const flags:FSEventStreamEventFlags);
procedure handleEvents( const originalSession:TDarwinFSWatchEventSession );
procedure doCallback( const watchPath:String; const internalEvent:TInternalEvent );
procedure updateStream;
procedure closeStream;
@ -112,20 +144,31 @@ public
procedure clearPath;
end;
implementation
const
kFSEventStreamCreateFlagUseExtendedData = $00000040;
kFSEventStreamEventFlagItemIsHardlink = $00100000;
kFSEventStreamEventFlagItemIsLastHardlink = $00200000;
kFSEventStreamEventFlagItemCloned = $00400000;
var
kFSEventStreamEventExtendedDataPathKey: CFStringRef;
kFSEventStreamEventExtendedFileIDKey: CFStringRef;
function StringToNSString(const S: String): NSString;
begin
Result:= NSString(NSString.stringWithUTF8String(PAnsiChar(S)));
end;
constructor TDarwinFSWatchEvent.create( const aWatchPath:String; const aFullPath:String; const aFlags:UInt32 );
constructor TDarwinFSWatchEvent.create( const aWatchPath:String; const internalEvent:TInternalEvent );
begin
Inherited Create;
_watchPath:= aWatchPath;
_fullPath:= aFullPath;
_rawEventFlags:= aFlags;
_fullPath:= internalEvent.path;
_renamedPath:= internalEvent.renamedPath;
_rawEventFlags:= internalEvent.flags;
_rawINode:= internalEvent.iNode;
createCategories;
end;
@ -156,8 +199,10 @@ begin
if (_rawEventFlags and kFSEventStreamEventFlagRootChanged)<>0 then begin
_categories:= _categories + [ecRootChanged];
end else begin
if (_fullPath<>watchPath) and (_fullPath.CountChar(PathDelim)<>_watchPath.CountChar(PathDelim)+1) then
_categories:= _categories + [ecChildChanged];
if (_fullPath<>watchPath) and (_fullPath.CountChar(PathDelim)<>_watchPath.CountChar(PathDelim)+1) then begin
if (_renamedPath.IsEmpty) or ((_renamedPath<>watchPath) and (_renamedPath.CountChar(PathDelim)<>_watchPath.CountChar(PathDelim)+1)) then
_categories:= _categories + [ecChildChanged];
end;
end;
if _categories * [ecAttribChanged,ecStructChanged,ecRootChanged] = [] then
@ -228,9 +273,9 @@ begin
Result:= Result + '|IsDir';
if (_rawEventFlags and kFSEventStreamEventFlagItemIsSymlink)<>0 then
Result:= Result + '|IsSymlink';
if (_rawEventFlags and $00100000)<>0 then
if (_rawEventFlags and kFSEventStreamEventFlagItemIsHardlink)<>0 then
Result:= Result + '|IsHardLink';
if (_rawEventFlags and $00200000)<>0 then
if (_rawEventFlags and kFSEventStreamEventFlagItemIsLastHardlink)<>0 then
Result:= Result + '|IsLastHardLink';
if (_rawEventFlags and kFSEventStreamEventFlagMustScanSubDirs)<>0 then
@ -249,7 +294,7 @@ begin
Result:= Result + '|Unmount';
if (_rawEventFlags and kFSEventStreamEventFlagOwnEvent)<>0 then
Result:= Result + '|OwnEvent';
if (_rawEventFlags and $00400000)<>0 then
if (_rawEventFlags and kFSEventStreamEventFlagItemCloned)<>0 then
Result:= Result + '|Cloned';
if (_rawEventFlags and $FF800000)<>0 then
@ -261,6 +306,153 @@ begin
Result:= Result.TrimLeft( '|' );
end;
// Note: try to avoid string copy
function isMatchWatchPath(
const internalEvent:TInternalEvent;
const watchPath:String;
const watchSubtree:Boolean ): Boolean;
var
fullPath: String;
fullPathDeep: Integer;
watchPathDeep: Integer;
begin
fullPath:= internalEvent.path;;
// detect if fullPath=watchPath
if (internalEvent.flags and (kFSEventStreamEventFlagItemIsDir or kFSEventStreamEventFlagRootChanged)) <> 0 then
begin
Result:= watchPath.Equals( fullPath );
if Result then exit; // fullPath=watchPath, matched
end;
// detect if fullPath startsWith watchPath
Result:= fullPath.StartsWith(watchPath);
if watchSubtree then exit;
// not watchSubtree
// not startsWith watchPath, not match
if not Result then exit;
// not watchSubtree, and startsWith watchPath
// detect if fullPath and watchPath in the same level
fullPathDeep:= fullPath.CountChar(PathDelim);
watchPathDeep:= watchPath.CountChar(PathDelim)+1;
Result:= fullPathDeep=watchPathDeep;
end;
function TInternalEvent.deepCopy(): TInternalEvent;
begin
Result:= TInternalEvent.Create;
Result.path:= self.path;
Result.renamedPath:= self.renamedPath;
Result.flags:= self.flags;
Result.iNode:= self.iNode;
end;
function TDarwinFSWatchEventSession.deepCopy(): TDarwinFSWatchEventSession;
var
list: TObjectList;
i: Integer;
begin
list:= TObjectList.Create;
for i:=0 to count-1 do
begin
list.Add( Items[i].deepCopy() );
end;
Result:= TDarwinFSWatchEventSession.create;
Result._list:= list;
end;
constructor TDarwinFSWatchEventSession.create();
begin
Inherited;
end;
constructor TDarwinFSWatchEventSession.create( const amount:size_t; eventPaths:CFArrayRef; flags:FSEventStreamEventFlagsPtr );
var
i: size_t;
event: TInternalEvent;
infoDict: CFDictionaryRef;
nsPath: NSString;
nsNode: CFNumberRef;
begin
_list:= TObjectList.Create;
for i:=0 to amount-1 do
begin
infoDict:= CFArrayGetValueAtIndex( eventPaths, i );
nsPath:= CFDictionaryGetValue( infoDict, kFSEventStreamEventExtendedDataPathKey );
nsNode:= CFDictionaryGetValue( infoDict, kFSEventStreamEventExtendedFileIDKey );
event:= TInternalEvent.Create;
event.path:= nsPath.UTF8String;
event.flags:= flags^;
if Assigned(nsNode) then
CFNumberGetValue( nsNode, kCFNumberLongLongType, @(event.iNode) );
_list.Add( event );
inc(flags);
end;
end;
destructor TDarwinFSWatchEventSession.destroy;
begin
FreeAndNil( _list );
end;
function TDarwinFSWatchEventSession.count: Integer;
begin
Result:= _list.Count;
end;
function TDarwinFSWatchEventSession.getItem( index:Integer ): TInternalEvent;
begin
Result:= TInternalEvent( _list[index] );
end;
function TDarwinFSWatchEventSession.isRenamed( event:TInternalEvent ): Boolean;
begin
Result:= event.flags
and (kFSEventStreamEventFlagItemRenamed or kFSEventStreamEventFlagItemCreated or kFSEventStreamEventFlagItemRemoved)
= kFSEventStreamEventFlagItemRenamed;
end;
procedure TDarwinFSWatchEventSession.adjustRenamedEventIfNecessary( index:Integer );
var
currentEvent: TInternalEvent;
nextEvent: TInternalEvent;
i: Integer;
begin
currentEvent:= Items[index];
if not isRenamed(currentEvent)
then exit;
// find all related Renamed Event, and try to build a complete Renamed Event with NewPath
i:= index + 1;
while i < count do
begin
nextEvent:= Items[i];
if isRenamed(nextEvent) and (nextEvent.iNode=currentEvent.iNode) then begin
if currentEvent.path<>nextEvent.path then currentEvent.renamedPath:= nextEvent.path;
_list.Delete( i );
end else begin
inc( i );
end;
end;
// got the complete Renamed Event, then exit
if not currentEvent.renamedPath.IsEmpty then
exit;
// got the incomplete Renamed Event, change to Created or Removed Event
currentEvent.flags:= currentEvent.flags and (not kFSEventStreamEventFlagItemRenamed);
if FpAccess(currentEvent.path, F_OK) = 0 then begin
currentEvent.flags:= currentEvent.flags or kFSEventStreamEventFlagItemCreated;
end else begin
currentEvent.flags:= currentEvent.flags or kFSEventStreamEventFlagItemRemoved;
end;
end;
constructor TDarwinFSWatcher.create( const callback:TDarwinFSWatchCallBack; const watchSubtree:Boolean; const latency:Integer );
begin
Inherited Create;
@ -289,74 +481,56 @@ begin
Inherited;
end;
procedure cdeclFSEventsCallback( {%H-}streamRef: ConstFSEventStreamRef;
procedure cdeclFSEventsCallback(
{%H-}streamRef: ConstFSEventStreamRef;
clientCallBackInfo: UnivPtr;
numEvents: size_t;
eventPaths: UnivPtr;
eventFlags: FSEventStreamEventFlagsPtr;
eventIds: FSEventStreamEventIdPtr ); cdecl;
{%H-}eventIds: FSEventStreamEventIdPtr ); cdecl;
var
watcher: TDarwinFSWatcher absolute clientCallBackInfo;
session: TDarwinFSWatchEventSession;
begin
watcher.handleEvents( NumEvents, PPChar(EventPaths), EventFlags, EventIds );
session:= TDarwinFSWatchEventSession.create( numEvents, eventPaths, eventFlags );
watcher.handleEvents( session );
FreeAndNil( session );
end;
procedure TDarwinFSWatcher.handleEvents( const amount:size_t; paths:PPChar; flags:FSEventStreamEventFlagsPtr; ids:FSEventStreamEventIdPtr );
procedure TDarwinFSWatcher.handleEvents( const originalSession:TDarwinFSWatchEventSession );
var
i: size_t;
nsWatchPath: NSString;
watchPath: String;
event: TInternalEvent;
i: Integer;
session: TDarwinFSWatchEventSession;
begin
for i:=amount downto 1 do
for nsWatchPath in _streamPaths do
begin
doCallback( paths^, flags^ );
inc(paths);
inc(flags);
watchPath:= nsWatchPath.UTF8String;
session:= originalSession.deepCopy();
i:= 0;
while i < session.count do
begin
event:= session[i];
if isMatchWatchPath(event, watchPath, _watchSubtree) then
begin
session.adjustRenamedEventIfNecessary( i );
doCallback( watchPath, event );
end;
inc( i );
end;
session.Free;
end;
end;
// Note: try to avoid string copy
function isMatchWatchPath(
const fullPath:String; const watchPath:String;
const flags:FSEventStreamEventFlags; const watchSubtree:Boolean ): Boolean;
procedure TDarwinFSWatcher.doCallback( const watchPath:String; const internalEvent:TInternalEvent );
var
fullPathDeep: Integer;
watchPathDeep: Integer;
begin
// detect if fullPath=watchPath
if (flags and (kFSEventStreamEventFlagItemIsDir or kFSEventStreamEventFlagRootChanged)) <> 0 then
begin
Result:= watchPath.Equals( fullPath );
if Result then exit; // fullPath=watchPath, matched
end;
// detect if fullPath startsWith watchPath
Result:= fullPath.StartsWith(watchPath);
if watchSubtree then exit;
// not watchSubtree
// not startsWith watchPath, not match
if not Result then exit;
// not watchSubtree, and startsWith watchPath
// detect if fullPath and watchPath in the same level
fullPathDeep:= fullPath.CountChar(PathDelim);
watchPathDeep:= watchPath.CountChar(PathDelim)+1;
Result:= fullPathDeep=watchPathDeep;
end;
procedure TDarwinFSWatcher.doCallback( const path:String; const flags:FSEventStreamEventFlags );
var
watchPath: NSString;
event: TDarwinFSWatchEvent;
begin
for watchPath in _streamPaths do
begin
if isMatchWatchPath(path, watchPath.UTF8String, flags, _watchSubtree) then
begin
event:= TDarwinFSWatchEvent.create( watchPath.UTF8String, path, flags );
_callback( event );
event.Free;
end;
end;
event:= TDarwinFSWatchEvent.create( watchPath, internalEvent );
_callback( event );
event.Free;
end;
procedure TDarwinFSWatcher.updateStream;
@ -376,7 +550,10 @@ begin
exit;
end;
flags:= kFSEventStreamCreateFlagFileEvents or kFSEventStreamCreateFlagWatchRoot;
flags:= kFSEventStreamCreateFlagFileEvents
or kFSEventStreamCreateFlagWatchRoot
or kFSEventStreamCreateFlagUseCFTypes
or kFSEventStreamCreateFlagUseExtendedData;
_stream:= FSEventStreamCreate( nil,
@cdeclFSEventsCallback,
@_streamContext,
@ -439,6 +616,7 @@ end;
procedure TDarwinFSWatcher.interrupt;
begin
Sleep( 20 );
if Assigned(_stream) then
CFRunLoopStop( _runLoop )
else
@ -502,5 +680,9 @@ begin
_pathsSyncObject.SetEvent;
end;
initialization
kFSEventStreamEventExtendedDataPathKey:= CFSTR('path');
kFSEventStreamEventExtendedFileIDKey:= CFSTR('fileID');
end.