Appropriate and improve VDKQueue (#4202)

This commit is contained in:
A Cœur 2022-11-19 03:53:36 +08:00 committed by GitHub
parent 6fcdb526c7
commit a152d0f6d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 430 additions and 595 deletions

View File

@ -13,9 +13,6 @@ third-party/*
# maintained at https://github.com/sparkle-project/Sparkle
macosx/Sparkle.framework/*
# maintained at https://github.com/bdkjones/VDKQueue
macosx/VDKQueue/*
# maintained at https://github.com/mnunberg/jsonsl
libtransmission/jsonsl.*

View File

@ -220,7 +220,7 @@
A2AAB65E0DE0CF6200E04DDA /* rpc-server.h in Headers */ = {isa = PBXBuildFile; fileRef = A2AAB65A0DE0CF6200E04DDA /* rpc-server.h */; };
A2AAB65F0DE0CF6200E04DDA /* rpcimpl.cc in Sources */ = {isa = PBXBuildFile; fileRef = A2AAB65B0DE0CF6200E04DDA /* rpcimpl.cc */; };
A2AAB6650DE0D08B00E04DDA /* blocklist.h in Headers */ = {isa = PBXBuildFile; fileRef = A2D307930D9EC4860051FD27 /* blocklist.h */; };
A2AB883E16A399A6008FAD50 /* VDKQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = A2AB883C16A399A6008FAD50 /* VDKQueue.m */; };
A2AB883E16A399A6008FAD50 /* VDKQueue.mm in Sources */ = {isa = PBXBuildFile; fileRef = A2AB883C16A399A6008FAD50 /* VDKQueue.mm */; };
A2AF1C390A3D0F6200F1575D /* FileOutlineView.mm in Sources */ = {isa = PBXBuildFile; fileRef = A2AF1C370A3D0F6200F1575D /* FileOutlineView.mm */; };
A2AF23C816B44FA0003BC59E /* log.cc in Sources */ = {isa = PBXBuildFile; fileRef = A2AF23C616B44FA0003BC59E /* log.cc */; };
A2AF23C916B44FA0003BC59E /* log.h in Headers */ = {isa = PBXBuildFile; fileRef = A2AF23C716B44FA0003BC59E /* log.h */; };
@ -976,7 +976,7 @@
A2AAB65A0DE0CF6200E04DDA /* rpc-server.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "rpc-server.h"; sourceTree = "<group>"; };
A2AAB65B0DE0CF6200E04DDA /* rpcimpl.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = rpcimpl.cc; sourceTree = "<group>"; };
A2AB883B16A399A6008FAD50 /* VDKQueue.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VDKQueue.h; sourceTree = "<group>"; };
A2AB883C16A399A6008FAD50 /* VDKQueue.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VDKQueue.m; sourceTree = "<group>"; };
A2AB883C16A399A6008FAD50 /* VDKQueue.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = VDKQueue.mm; sourceTree = "<group>"; };
A2AF1C360A3D0F6200F1575D /* FileOutlineView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FileOutlineView.h; sourceTree = "<group>"; };
A2AF1C370A3D0F6200F1575D /* FileOutlineView.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FileOutlineView.mm; sourceTree = "<group>"; };
A2AF23C616B44FA0003BC59E /* log.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = log.cc; sourceTree = "<group>"; };
@ -1843,7 +1843,7 @@
isa = PBXGroup;
children = (
A2AB883B16A399A6008FAD50 /* VDKQueue.h */,
A2AB883C16A399A6008FAD50 /* VDKQueue.m */,
A2AB883C16A399A6008FAD50 /* VDKQueue.mm */,
);
path = VDKQueue;
sourceTree = "<group>";
@ -3056,7 +3056,7 @@
A22BAE281388040500FB022F /* NSMutableArrayAdditions.mm in Sources */,
A2966E8713DAF74C007B52DF /* GlobalOptionsPopoverViewController.mm in Sources */,
A234EA541453563B000F3E97 /* NSImageAdditions.mm in Sources */,
A2AB883E16A399A6008FAD50 /* VDKQueue.m in Sources */,
A2AB883E16A399A6008FAD50 /* VDKQueue.mm in Sources */,
A2451E6916ACE4EB00586E0E /* FileRenameSheetController.mm in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@ -399,7 +399,7 @@ add_definitions(
)
add_library(vdkqueue STATIC
VDKQueue/VDKQueue.m
VDKQueue/VDKQueue.mm
VDKQueue/VDKQueue.h
)

View File

@ -1,69 +0,0 @@
VDKQueue
=======
A modern, faster, better version of UKKQueue.
<http://incident57.com/codekit>
about
-----
VDKQueue is an Objective-C wrapper around kernel queues (kQueues).
It allows you to watch a file or folder for changes and be notified when they occur.
VDKQueue is a modern, streamlined and much faster version of UKKQueue, which was originally written in 2003 by Uli Kusterer.
Objective-C has come a long way in the past nine years and UKKQueue was long in the tooth. VDKQueue is better in several ways:
-- The number of method calls is vastly reduced.
-- Grand Central Dispatch is used in place of Uli's "threadProxy" notifications (much faster)
-- Memory footprint is roughly halved, since VDKQueue creates less overhead
-- Fewer locks are taken, especially in loops (faster)
-- The code is *much* cleaner and simpler!
-- There is only one .h and one .m file to include.
VDKQueue also fixes long-standing bugs in UKKQueue. For example: OS X limits the number of open file descriptors each process
may have to about 3,000. If UKKQueue fails to open a new file descriptor because it has hit this limit, it will crash. VDKQueue will not.
performance
-----------
Adding 1,945 file paths to a UKKQueue instance took, on average, 80ms.
Adding those same files to a VDKQueue instance took, on average, 65ms.
VDKQueue processes and pushes out notifications about file changes roughly 50-70% faster than UKKQueue.
All tests conducted on a 2008 MacBook Pro 2.5Ghz with 4GB of RAM running OS 10.7.3 using Xcode and Instruments (time profiler).
requirements
------------
VDKQueue requires Mac OS X 10.6+ because it uses Grand Central Dispatch.
VDKQueue does not support garbage collection. If you use garbage collection, you are lazy. Shape up.
VDKQueue does not currently use ARC, although it should be straightforward to convert if you wish. (Don't be the guy that can't manually manage memory, though.)
license
-------
Created by Bryan D K Jones on 28 March 2012
Copyright 2013 Bryan D K Jones
Based heavily on UKKQueue, which was created and copyrighted by Uli Kusterer on 21 Dec 2003.
This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.

191
macosx/VDKQueue/VDKQueue.h Executable file → Normal file
View File

@ -1,8 +1,8 @@
// VDKQueue.h
// Created by Bryan D K Jones on 28 March 2012
// Copyright 2013 Bryan D K Jones
// Copyright © 2017-2022 Transmission authors and contributors.
//
// Based heavily on UKKQueue, which was created and copyrighted by Uli Kusterer on 21 Dec 2003.
// Based on VDKQueue (https://github.com/bdkjones/VDKQueue) which was created and copyrighted by Bryan D K Jones on 28 March 2012.
// Based on UKKQueue (https://github.com/uliwitness/UKFileWatcher) which was created and copyrighted by Uli Kusterer on 21 Dec 2003.
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
@ -19,133 +19,134 @@
// 3. This notice may not be removed or altered from any source
// distribution.
// DESCRIPTION
//
// BASED ON UKKQUEUE:
//
// This is an updated, modernized and streamlined version of the excellent UKKQueue class, which was authored by Uli Kusterer.
// UKKQueue was written back in 2003 and there have been many, many improvements to Objective-C since then. VDKQueue uses the
// core of Uli's original class, but makes it faster and more efficient. Method calls are reduced. Grand Central Dispatch is used in place
// of Uli's "threadProxy" objects. The memory footprint is roughly halved, as I don't create the overhead that UKKQueue does.
//
// VDKQueue is also simplified. The option to use it as a singleton is removed. You simply alloc/init an instance and add paths you want to
// watch. Your objects can be alerted to changes either by notifications or by a delegate method (or both). See below.
//
// It also fixes several bugs. For one, it won't crash if it can't create a file descriptor to a file you ask it to watch. (By default, an OS X process can only
// have about 3,000 file descriptors open at once. If you hit that limit, UKKQueue will crash. VDKQueue will not.)
//
// VDKQueue is an Objective-C wrapper around kernel queues (kQueues). It allows you to watch a file or folder for changes and be notified when they occur.
// USAGE
//
// DEPENDENCIES:
//
// VDKQueue requires OS 10.6+ because it relies on Grand Central Dispatch.
//
// You simply alloc/init an instance and add paths you want to watch. Your objects can be alerted to changes either by notifications or by a delegate method (or both).
//
// IMPORTANT NOTE ABOUT ATOMIC OPERATIONS
//
// There are two ways of saving a file on OS X: Atomic and Non-Atomic. In a non-atomic operation, a file is saved by directly overwriting it with new data.
// In an Atomic save, a temporary file is first written to a different location on disk. When that completes successfully, the original file is deleted and the
// temporary one is renamed and moved into place where the original file existed.
// There are two ways of saving a file on macOS: Atomic and Non-Atomic. In a non-atomic operation, a file is saved by directly overwriting it with new data.
// In an Atomic save, a temporary file is first written to a different location on disk. When that completes successfully, the original file is deleted and the temporary one is renamed and moved into place where the original file existed.
//
// This matters a great deal. If you tell VDKQueue to watch file X, then you save file X ATOMICALLY, you'll receive a notification about that event. HOWEVER, you will
// NOT receive any additional notifications for file X from then on. This is because the atomic operation has essentially created a new file that replaced the one you
// told VDKQueue to watch. (This is not an issue for non-atomic operations.)
// This matters a great deal. If you tell VDKQueue to watch file X, then you save file X ATOMICALLY, you'll receive a notification about that event. HOWEVER, you will NOT receive any additional notifications for file X from then on. This is because the atomic operation has essentially created a new file that replaced the one you told VDKQueue to watch. (This is not an issue for non-atomic operations.)
//
// To handle this, any time you receive a change notification from VDKQueue, you should call -removePath: followed by -addPath: on the file's path, even if the path
// has not changed. This will ensure that if the event that triggered the notification was an atomic operation, VDKQueue will start watching the "new" file that took
// the place of the old one.
//
// Other frameworks out there try to work around this issue by immediately attempting to re-open the file descriptor to the path. This is not bulletproof and may fail;
// it all depends on the timing of disk I/O. Bottom line: you could not rely on it and might miss future changes to the file path you're supposedly watching. That's why
// VDKQueue does not take this approach, but favors the "manual" method of "stop-watching-then-rewatch".
// To handle this, any time you receive a change notification from VDKQueue, you should call -removePath: followed by -addPath: on the file's path, even if the path has not changed.
// This will ensure that if the event that triggered the notification was an atomic operation, VDKQueue will start watching the "new" file that took the place of the old one.
//
// Some try to work around this issue by immediately attempting to re-open the file descriptor to the path. This is not bulletproof and may fail; it all depends on the timing of disk I/O.
// Bottom line: you could not rely on it and might miss future changes to the file path you're supposedly watching. That's why VDKQueue does not take this approach, but favors the "manual" method of "stop-watching-then-rewatch".
// LIMITATIONS of VDKQueue
//
// - You have to manually call -removePath: followed by -addPath: each time you receive a change notification.
// - Callbacks are only on the main thread.
// - Unmaintained as a standalone project.
#warning adopt an alternative to VDKQueue (UKFSEventsWatcher, EonilFSEvents, FileWatcher, DTFolderMonitor or SFSMonitor)
// ALTERNATIVES (from archaic to modern)
//
// - FreeBSD 4.1: Kernel Queue API (kevent and kqueue)
// (https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/kqueue.2.html)
//
// Example: SKQueue (https://github.com/daniel-pedersen/SKQueue) but claimed to crash and be superseded by SFSMonitor (https://stackoverflow.com/a/62167224)
//
// - macOS 10.110.8: FNSubscribe and FNNotify API
// (https://developer.apple.com/documentation/coreservices/1566843-fnsubscribebypath)
// "the FNNotify API has been supplanted by the FSEvents API"
// (https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.7.sdk/System/Library/Frameworks/AppKit.framework/Versions/C/Headers/NSWorkspace.h)
//
// - macOS 10.5+: File System Events API (FSEventStreamCreate)
// (https://developer.apple.com/documentation/coreservices/file_system_events)
// "File system events are intended to provide notification of changes with directory-level granularity. For most purposes, this is sufficient. In some cases, however, you may need to receive notifications with finer granularity. For example, you might need to monitor only changes made to a single file. For that purpose, the kernel queue (kqueue) notification system is more appropriate.
// If you are monitoring a large hierarchy of content, you should use file system events instead, however, because kernel queues are somewhat more complex than kernel events, and can be more resource intensive because of the additional user-kernel communication involved."
// (https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/FSEvents_ProgGuide/KernelQueues/KernelQueues.html)
//
// Example: UKFSEventsWatcher (https://github.com/uliwitness/UKFileWatcher)
// Example: EonilFSEvents (https://github.com/eonil/FSEvents)
// Example: FileWatcher (https://github.com/eonist/FileWatcher)
//
// - macOS 10.6+: Grand Central Dispatch API to monitor virtual filesystem nodes (DISPATCH_SOURCE_TYPE_VNODE)
// (https://developer.apple.com/documentation/dispatch/dispatch_source_type_vnode)
// "GCD uses kqueue under the hood and the same capabilities are made available."
// (https://www.reddit.com/r/programming/comments/l6j3g/using_kqueue_in_cocoa/c2q74yy)
//
// Example: RSTDirectoryMonitor (https://github.com/varuzhnikov/HelloWorld) but unmaintained as a standalone project (abandoned 2013)
// Example: DirectoryMonitor (https://github.com/robovm/apple-ios-samples/blob/master/ListerforwatchOSiOSandOSX/Swift/ListerKit/DirectoryMonitor.swift) but unmaintained (abandoned 2016)
// Example: TABFileMonitor (https://github.com/tblank555/iMonitorMyFiles/tree/master/iMonitorMyFiles/Classes) but unmaintained (abandoned 2016)
// Example: DTFolderMonitor (https://github.com/Cocoanetics/DTFoundation/tree/develop/Core/Source)
//
// - macOS 10.7+: NSFilePresenter API
// (https://developer.apple.com/documentation/foundation/nsfilepresenter?language=objc)
// "They're buggy, broken, and Apple is haven't willing to fix them for last 4 years."
// (https://stackoverflow.com/a/26878163)
//
// - macOS 10.10+: DispatchSource API (makeFileSystemObjectSource)
// (https://developer.apple.com/documentation/dispatch/dispatchsource/2300040-makefilesystemobjectsource)
//
// Example: SFSMonitor (https://github.com/ClassicalDude/SFSMonitor)
#import <Foundation/Foundation.h>
#include <sys/types.h>
#include <sys/event.h>
@class VDKQueue;
//
// Logical OR these values into the u_int that you pass in the -addPath:notifyingAbout: method
// to specify the types of notifications you're interested in. Pass the default value to receive all of them.
//
#define VDKQueueNotifyAboutRename NOTE_RENAME // Item was renamed.
#define VDKQueueNotifyAboutWrite NOTE_WRITE // Item contents changed (also folder contents changed).
#define VDKQueueNotifyAboutDelete NOTE_DELETE // item was removed.
#define VDKQueueNotifyAboutAttributeChange NOTE_ATTRIB // Item attributes changed.
#define VDKQueueNotifyAboutSizeIncrease NOTE_EXTEND // Item size increased.
#define VDKQueueNotifyAboutLinkCountChanged NOTE_LINK // Item's link count changed.
#define VDKQueueNotifyAboutAccessRevocation NOTE_REVOKE // Access to item was revoked.
typedef NS_OPTIONS(u_int, VDKQueueNotify)
{
VDKQueueNotifyAboutRename = NOTE_RENAME, ///< Item was renamed.
VDKQueueNotifyAboutWrite = NOTE_WRITE, ///< Item contents changed (also folder contents changed).
VDKQueueNotifyAboutDelete = NOTE_DELETE, ///< Item was removed.
VDKQueueNotifyAboutAttributeChange = NOTE_ATTRIB, ///< Item attributes changed.
VDKQueueNotifyAboutSizeIncrease = NOTE_EXTEND, ///< Item size increased.
VDKQueueNotifyAboutLinkCountChanged = NOTE_LINK, ///< Item's link count changed.
VDKQueueNotifyAboutAccessRevocation = NOTE_REVOKE, ///< Access to item was revoked.
VDKQueueNotifyDefault = VDKQueueNotifyAboutRename | VDKQueueNotifyAboutWrite | VDKQueueNotifyAboutDelete |
VDKQueueNotifyAboutAttributeChange | VDKQueueNotifyAboutSizeIncrease | VDKQueueNotifyAboutLinkCountChanged | VDKQueueNotifyAboutAccessRevocation
};
#define VDKQueueNotifyDefault (VDKQueueNotifyAboutRename | VDKQueueNotifyAboutWrite \
| VDKQueueNotifyAboutDelete | VDKQueueNotifyAboutAttributeChange \
| VDKQueueNotifyAboutSizeIncrease | VDKQueueNotifyAboutLinkCountChanged \
| VDKQueueNotifyAboutAccessRevocation)
//
// Notifications that this class sends to the NSWORKSPACE notification center.
// Notifications that this class sends to the default notification center.
// Object = the instance of VDKQueue that was watching for changes
// userInfo.path = the file path where the change was observed
//
extern NSString * VDKQueueRenameNotification;
extern NSString * VDKQueueWriteNotification;
extern NSString * VDKQueueDeleteNotification;
extern NSString * VDKQueueAttributeChangeNotification;
extern NSString * VDKQueueSizeIncreaseNotification;
extern NSString * VDKQueueLinkCountChangeNotification;
extern NSString * VDKQueueAccessRevocationNotification;
extern NSString const* VDKQueueRenameNotification;
extern NSString const* VDKQueueWriteNotification;
extern NSString const* VDKQueueDeleteNotification;
extern NSString const* VDKQueueAttributeChangeNotification;
extern NSString const* VDKQueueSizeIncreaseNotification;
extern NSString const* VDKQueueLinkCountChangeNotification;
extern NSString const* VDKQueueAccessRevocationNotification;
// You can specify a delegate and implement this protocol's method to respond to kQueue events, instead of subscribing to notifications.
@protocol VDKQueueDelegate<NSObject>
//
// Or, instead of subscribing to notifications, you can specify a delegate and implement this method to respond to kQueue events.
// Note the required statement! For speed, this class does not check to make sure the delegate implements this method. (When I say "required" I mean it!)
//
@class VDKQueue;
@protocol VDKQueueDelegate <NSObject>
@required
-(void) VDKQueue:(VDKQueue *)queue receivedNotification:(NSString*)noteName forPath:(NSString*)fpath;
- (void)VDKQueue:(VDKQueue*)queue receivedNotification:(NSString*)noteName forPath:(NSString*)fpath;
@end
@interface VDKQueue : NSObject
{
id<VDKQueueDelegate> __weak _delegate;
BOOL _alwaysPostNotifications; // By default, notifications are posted only if there is no delegate set. Set this value to YES to have notes posted even when there is a delegate.
@private
int _coreQueueFD; // The actual kqueue ID (Unix file descriptor).
NSMutableDictionary *_watchedPathEntries; // List of VDKQueuePathEntries. Keys are NSStrings of the path that each VDKQueuePathEntry is for.
BOOL _keepWatcherThreadRunning; // Set to NO to cancel the thread that watches _coreQueueFD for kQueue events
}
// Note: there is no need to ask whether a path is already being watched.
// Just add it or remove it and this class will take action only if appropriate.
// (Add only if we're not already watching it, remove only if we are.)
//
// Note: there is no need to ask whether a path is already being watched. Just add it or remove it and this class
// will take action only if appropriate. (Add only if we're not already watching it, remove only if we are.)
//
// Warning: You must pass full, root-relative paths. Do not pass tilde-abbreviated paths or file URLs.
//
- (void) addPath:(NSString *)aPath;
- (void) addPath:(NSString *)aPath notifyingAbout:(u_int)flags; // See note above for values to pass in "flags"
// Warning: You must pass full, root-relative paths. Do not pass tilde-abbreviated paths or file URLs.
- (void)addPath:(NSString*)aPath;
- (void)addPath:(NSString*)aPath notifyingAbout:(u_int)flags; // See note above for values to pass in "flags"
- (void) removePath:(NSString *)aPath;
- (void) removeAllPaths;
- (void)removePath:(NSString*)aPath;
- (void)removeAllPaths;
@property(nonatomic, weak) id<VDKQueueDelegate> delegate;
@property (nonatomic, readonly) NSUInteger numberOfWatchedPaths; // Returns the number of paths that this VDKQueue instance is actively watching.
@property (weak) id<VDKQueueDelegate> delegate;
@property (assign) BOOL alwaysPostNotifications;
/// By default, notifications are posted only if there is no delegate set. Set this value to YES to have notes posted even when there is a delegate.
@property(nonatomic, assign) BOOL alwaysPostNotifications;
@end

View File

@ -1,423 +0,0 @@
// VDKQueue.m
// Created by Bryan D K Jones on 28 March 2012
// Copyright 2013 Bryan D K Jones
//
// Based heavily on UKKQueue, which was created and copyrighted by Uli Kusterer on 21 Dec 2003.
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source
// distribution.
#import <AppKit/AppKit.h>
#import "VDKQueue.h"
#import <unistd.h>
#import <fcntl.h>
#include <sys/stat.h>
NSString * VDKQueueRenameNotification = @"VDKQueueFileRenamedNotification";
NSString * VDKQueueWriteNotification = @"VDKQueueFileWrittenToNotification";
NSString * VDKQueueDeleteNotification = @"VDKQueueFileDeletedNotification";
NSString * VDKQueueAttributeChangeNotification = @"VDKQueueFileAttributesChangedNotification";
NSString * VDKQueueSizeIncreaseNotification = @"VDKQueueFileSizeIncreasedNotification";
NSString * VDKQueueLinkCountChangeNotification = @"VDKQueueLinkCountChangedNotification";
NSString * VDKQueueAccessRevocationNotification = @"VDKQueueAccessWasRevokedNotification";
#pragma mark -
#pragma mark VDKQueuePathEntry
#pragma mark -
#pragma ------------------------------------------------------------------------------------------------------------------------------------------------------------
// This is a simple model class used to hold info about each path we watch.
@interface VDKQueuePathEntry : NSObject
{
NSString* _path;
int _watchedFD;
u_int _subscriptionFlags;
}
- (instancetype) init NS_UNAVAILABLE;
- (instancetype) initWithPath:(NSString*)inPath andSubscriptionFlags:(u_int)flags NS_DESIGNATED_INITIALIZER;
@property (atomic, copy) NSString *path;
@property (atomic, assign) int watchedFD;
@property (atomic, assign) u_int subscriptionFlags;
@end
@implementation VDKQueuePathEntry
@synthesize path = _path, watchedFD = _watchedFD, subscriptionFlags = _subscriptionFlags;
- (instancetype) initWithPath:(NSString*)inPath andSubscriptionFlags:(u_int)flags;
{
self = [super init];
if (self)
{
_path = [inPath copy];
_watchedFD = open(_path.fileSystemRepresentation, O_EVTONLY, 0);
if (_watchedFD < 0)
{
return nil;
}
_subscriptionFlags = flags;
}
return self;
}
-(void) dealloc
{
if (_watchedFD >= 0) close(_watchedFD);
_watchedFD = -1;
}
@end
#pragma mark -
#pragma mark VDKQueue
#pragma mark -
#pragma ------------------------------------------------------------------------------------------------------------------------------------------------------------
@interface VDKQueue ()
- (void) watcherThread:(id)sender;
@end
@implementation VDKQueue
@synthesize delegate = _delegate, alwaysPostNotifications = _alwaysPostNotifications;
#pragma mark -
#pragma mark INIT/DEALLOC
- (instancetype) init
{
self = [super init];
if (self)
{
_coreQueueFD = kqueue();
if (_coreQueueFD == -1)
{
return nil;
}
_alwaysPostNotifications = NO;
_watchedPathEntries = [[NSMutableDictionary alloc] init];
}
return self;
}
- (void) dealloc
{
// Shut down the thread that's scanning for kQueue events
_keepWatcherThreadRunning = NO;
// Do this to close all the open file descriptors for files we're watching
[self removeAllPaths];
_watchedPathEntries = nil;
}
#pragma mark -
#pragma mark PRIVATE METHODS
- (VDKQueuePathEntry *) addPathToQueue:(NSString *)path notifyingAbout:(u_int)flags
{
@synchronized(self)
{
// Are we already watching this path?
VDKQueuePathEntry *pathEntry = _watchedPathEntries[path];
if (pathEntry)
{
// All flags already set?
if((pathEntry.subscriptionFlags & flags) == flags)
{
return pathEntry;
}
flags |= pathEntry.subscriptionFlags;
}
struct timespec nullts = { 0, 0 };
struct kevent ev;
if (!pathEntry)
{
pathEntry = [[VDKQueuePathEntry alloc] initWithPath:path andSubscriptionFlags:flags];
}
if (pathEntry)
{
EV_SET(&ev, [pathEntry watchedFD], EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_CLEAR, flags, 0, (__bridge void *) pathEntry);
pathEntry.subscriptionFlags = flags;
_watchedPathEntries[path] = pathEntry;
kevent(_coreQueueFD, &ev, 1, NULL, 0, &nullts);
// Start the thread that fetches and processes our events if it's not already running.
if(!_keepWatcherThreadRunning)
{
_keepWatcherThreadRunning = YES;
[NSThread detachNewThreadSelector:@selector(watcherThread:) toTarget:self withObject:nil];
}
}
return pathEntry;
}
return nil;
}
- (void) watcherThread:(id)sender
{
int n;
struct kevent ev;
struct timespec timeout = { 1, 0 }; // 1 second timeout. Should be longer, but we need this thread to exit when a kqueue is dealloced, so 1 second timeout is quite a while to wait.
int theFD = _coreQueueFD; // So we don't have to risk accessing iVars when the thread is terminated.
NSMutableArray *notesToPost = [[NSMutableArray alloc] initWithCapacity:5];
#if DEBUG_LOG_THREAD_LIFETIME
NSLog(@"watcherThread started.");
#endif
while(_keepWatcherThreadRunning)
{
@try
{
n = kevent(theFD, NULL, 0, &ev, 1, &timeout);
if (n > 0)
{
//NSLog( @"KEVENT returned %d", n );
if (ev.filter == EVFILT_VNODE)
{
//NSLog( @"KEVENT filter is EVFILT_VNODE" );
if (ev.fflags)
{
//NSLog( @"KEVENT flags are set" );
//
// Note: VDKQueue gets tested by thousands of CodeKit users who each watch several thousand files at once.
// I was receiving about 3 EXC_BAD_ACCESS (SIGSEGV) crash reports a month that listed the 'path' objc_msgSend
// as the culprit. That suggests the KEVENT is being sent back to us with a udata value that is NOT what we assigned
// to the queue, though I don't know why and I don't know why it's intermittent. Regardless, I've added an extra
// check here to try to eliminate this (infrequent) problem. In theory, a KEVENT that does not have a VDKQueuePathEntry
// object attached as the udata parameter is not an event we registered for, so we should not be "missing" any events. In theory.
//
id pe = (__bridge id)(ev.udata);
if (pe && [pe respondsToSelector:@selector(path)])
{
NSString *fpath = ((VDKQueuePathEntry *)pe).path;
if (!fpath) continue;
[[NSWorkspace sharedWorkspace] noteFileSystemChanged:fpath];
// Clear any old notifications
[notesToPost removeAllObjects];
// Figure out which notifications we need to issue
if ((ev.fflags & NOTE_RENAME) == NOTE_RENAME)
{
[notesToPost addObject:VDKQueueRenameNotification];
}
if ((ev.fflags & NOTE_WRITE) == NOTE_WRITE)
{
[notesToPost addObject:VDKQueueWriteNotification];
}
if ((ev.fflags & NOTE_DELETE) == NOTE_DELETE)
{
[notesToPost addObject:VDKQueueDeleteNotification];
}
if ((ev.fflags & NOTE_ATTRIB) == NOTE_ATTRIB)
{
[notesToPost addObject:VDKQueueAttributeChangeNotification];
}
if ((ev.fflags & NOTE_EXTEND) == NOTE_EXTEND)
{
[notesToPost addObject:VDKQueueSizeIncreaseNotification];
}
if ((ev.fflags & NOTE_LINK) == NOTE_LINK)
{
[notesToPost addObject:VDKQueueLinkCountChangeNotification];
}
if ((ev.fflags & NOTE_REVOKE) == NOTE_REVOKE)
{
[notesToPost addObject:VDKQueueAccessRevocationNotification];
}
NSArray *notes = [[NSArray alloc] initWithArray:notesToPost]; // notesToPost will be changed in the next loop iteration, which will likely occur before the block below runs.
// Post the notifications (or call the delegate method) on the main thread.
dispatch_async(dispatch_get_main_queue(),
^{
for (NSString *note in notes)
{
[_delegate VDKQueue:self receivedNotification:note forPath:fpath];
if (!_delegate || _alwaysPostNotifications)
{
NSDictionary * userInfoDict = @{@"path": fpath};
[[NSWorkspace sharedWorkspace].notificationCenter postNotificationName:note object:self userInfo:userInfoDict];
}
}
});
}
}
}
}
}
@catch (NSException *localException)
{
NSLog(@"Error in VDKQueue watcherThread: %@", localException);
}
}
// Close our kqueue's file descriptor
if(close(theFD) == -1) {
NSLog(@"VDKQueue watcherThread: Couldn't close main kqueue (%d)", errno);
}
#if DEBUG_LOG_THREAD_LIFETIME
NSLog(@"watcherThread finished.");
#endif
}
#pragma mark -
#pragma mark PUBLIC METHODS
#pragma -----------------------------------------------------------------------------------------------------------------------------------------------------
- (void) addPath:(NSString *)aPath
{
if (!aPath) return;
@synchronized(self)
{
VDKQueuePathEntry *entry = _watchedPathEntries[aPath];
// Only add this path if we don't already have it.
if (!entry)
{
entry = [self addPathToQueue:aPath notifyingAbout:VDKQueueNotifyDefault];
if (!entry) {
NSLog(@"VDKQueue tried to add the path %@ to watchedPathEntries, but the VDKQueuePathEntry was nil. \nIt's possible that the host process has hit its max open file descriptors limit.", aPath);
}
}
}
}
- (void) addPath:(NSString *)aPath notifyingAbout:(u_int)flags
{
if (!aPath) return;
@synchronized(self)
{
VDKQueuePathEntry *entry = _watchedPathEntries[aPath];
// Only add this path if we don't already have it.
if (!entry)
{
entry = [self addPathToQueue:aPath notifyingAbout:flags];
if (!entry) {
NSLog(@"VDKQueue tried to add the path %@ to watchedPathEntries, but the VDKQueuePathEntry was nil. \nIt's possible that the host process has hit its max open file descriptors limit.", aPath);
}
}
}
}
- (void) removePath:(NSString *)aPath
{
if (!aPath) return;
@synchronized(self)
{
VDKQueuePathEntry *entry = _watchedPathEntries[aPath];
// Remove it only if we're watching it.
if (entry) {
[_watchedPathEntries removeObjectForKey:aPath];
}
}
}
- (void) removeAllPaths
{
@synchronized(self)
{
[_watchedPathEntries removeAllObjects];
}
}
- (NSUInteger) numberOfWatchedPaths
{
NSUInteger count;
@synchronized(self)
{
count = _watchedPathEntries.count;
}
return count;
}
@end

329
macosx/VDKQueue/VDKQueue.mm Normal file
View File

@ -0,0 +1,329 @@
// VDKQueue.mm
// Copyright © 2017-2022 Transmission authors and contributors.
//
// Based on VDKQueue (https://github.com/bdkjones/VDKQueue) which was created and copyrighted by Bryan D K Jones on 28 March 2012.
// Based on UKKQueue (https://github.com/uliwitness/UKFileWatcher) which was created and copyrighted by Uli Kusterer on 21 Dec 2003.
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source
// distribution.
#import "VDKQueue.h"
#import <unistd.h>
#import <fcntl.h>
#include <sys/stat.h>
NSString const* VDKQueueRenameNotification = @"VDKQueueFileRenamedNotification";
NSString const* VDKQueueWriteNotification = @"VDKQueueFileWrittenToNotification";
NSString const* VDKQueueDeleteNotification = @"VDKQueueFileDeletedNotification";
NSString const* VDKQueueAttributeChangeNotification = @"VDKQueueFileAttributesChangedNotification";
NSString const* VDKQueueSizeIncreaseNotification = @"VDKQueueFileSizeIncreasedNotification";
NSString const* VDKQueueLinkCountChangeNotification = @"VDKQueueLinkCountChangedNotification";
NSString const* VDKQueueAccessRevocationNotification = @"VDKQueueAccessWasRevokedNotification";
#pragma mark -
#pragma mark VDKQueuePathEntry
/// This is a simple model class used to hold info about each path we watch.
@interface VDKQueuePathEntry : NSObject
@property(atomic, copy) NSString* path;
@property(atomic, assign) int watchedFD;
@property(atomic, assign) u_int subscriptionFlags;
@end
@implementation VDKQueuePathEntry
- (nullable instancetype)initWithPath:(NSString*)inPath andSubscriptionFlags:(u_int)flags
{
self = [super init];
if (self)
{
_path = [inPath copy];
_watchedFD = open(_path.fileSystemRepresentation, O_EVTONLY, 0);
if (_watchedFD < 0)
{
return nil;
}
_subscriptionFlags = flags;
}
return self;
}
- (void)dealloc
{
if (_watchedFD >= 0)
{
close(_watchedFD);
}
_watchedFD = -1;
}
@end
#pragma mark -
#pragma mark VDKQueue
@interface VDKQueue ()
{
@private
/// The actual kqueue ID (Unix file descriptor).
int _coreQueueFD;
/// List of VDKQueuePathEntries. Keys are NSStrings of the path that each VDKQueuePathEntry is for.
NSMutableDictionary* _watchedPathEntries;
/// Set to NO to cancel the thread that watches `_coreQueueFD` for kQueue events
BOOL _keepWatcherThreadRunning;
}
@end
@implementation VDKQueue
#pragma mark -
#pragma mark INIT/DEALLOC
- (instancetype)init
{
self = [super init];
if (self)
{
_coreQueueFD = kqueue();
if (_coreQueueFD == -1)
{
return nil;
}
_watchedPathEntries = [[NSMutableDictionary alloc] init];
}
return self;
}
- (void)dealloc
{
// Shut down the thread that's scanning for kQueue events
_keepWatcherThreadRunning = NO;
// Do this to close all the open file descriptors for files we're watching
[self removeAllPaths];
_watchedPathEntries = nil;
}
#pragma mark -
#pragma mark PRIVATE METHODS
- (VDKQueuePathEntry*)addPathToQueue:(NSString*)path notifyingAbout:(u_int)flags
{
@synchronized(self)
{
// Are we already watching this path?
VDKQueuePathEntry* pathEntry = _watchedPathEntries[path];
if (pathEntry)
{
// All flags already set?
if ((pathEntry.subscriptionFlags & flags) == flags)
{
return pathEntry;
}
flags |= pathEntry.subscriptionFlags;
}
if (!pathEntry)
{
pathEntry = [[VDKQueuePathEntry alloc] initWithPath:path andSubscriptionFlags:flags];
}
if (pathEntry)
{
struct timespec nullts = { 0, 0 };
struct kevent ev;
EV_SET(&ev, pathEntry.watchedFD, EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_CLEAR, flags, 0, (__bridge void*)pathEntry);
pathEntry.subscriptionFlags = flags;
_watchedPathEntries[path] = pathEntry;
kevent(_coreQueueFD, &ev, 1, NULL, 0, &nullts);
// Start the thread that fetches and processes our events if it's not already running.
if (!_keepWatcherThreadRunning)
{
_keepWatcherThreadRunning = YES;
[NSThread detachNewThreadSelector:@selector(watcherThread:) toTarget:self withObject:nil];
}
}
return pathEntry;
}
}
- (void)watcherThread:(id)sender
{
int n;
struct kevent ev;
// 1 second timeout. Should be longer, but we need this thread to exit when a kqueue is dealloced, so 1 second timeout is quite a while to wait.
struct timespec timeout = { 1, 0 };
// So we don't have to risk accessing iVars when the thread is terminated.
int theFD = _coreQueueFD;
NSMutableArray* notesToPost = [[NSMutableArray alloc] initWithCapacity:5];
#if DEBUG_LOG_THREAD_LIFETIME
NSLog(@"watcherThread started.");
#endif
while (_keepWatcherThreadRunning)
{
n = kevent(theFD, NULL, 0, &ev, 1, &timeout);
if (n <= 0 || ev.filter != EVFILT_VNODE || !ev.fflags)
{
continue;
}
// The KEVENT can be sent back to us with a udata value that is NOT what we assigned to the queue.
// A KEVENT that does not have a VDKQueuePathEntry object attached as the udata parameter is not an event we registered for.
id pe = (__bridge id)(ev.udata);
if (![pe isKindOfClass:VDKQueuePathEntry.class])
{
continue;
}
NSString* fpath = ((VDKQueuePathEntry*)pe).path;
if (!fpath)
{
continue;
}
// Clear any old notifications
[notesToPost removeAllObjects];
// Figure out which notifications we need to issue
if ((ev.fflags & NOTE_RENAME) == NOTE_RENAME)
{
[notesToPost addObject:VDKQueueRenameNotification];
}
if ((ev.fflags & NOTE_WRITE) == NOTE_WRITE)
{
[notesToPost addObject:VDKQueueWriteNotification];
}
if ((ev.fflags & NOTE_DELETE) == NOTE_DELETE)
{
[notesToPost addObject:VDKQueueDeleteNotification];
}
if ((ev.fflags & NOTE_ATTRIB) == NOTE_ATTRIB)
{
[notesToPost addObject:VDKQueueAttributeChangeNotification];
}
if ((ev.fflags & NOTE_EXTEND) == NOTE_EXTEND)
{
[notesToPost addObject:VDKQueueSizeIncreaseNotification];
}
if ((ev.fflags & NOTE_LINK) == NOTE_LINK)
{
[notesToPost addObject:VDKQueueLinkCountChangeNotification];
}
if ((ev.fflags & NOTE_REVOKE) == NOTE_REVOKE)
{
[notesToPost addObject:VDKQueueAccessRevocationNotification];
}
// notesToPost will be changed in the next loop iteration, which will likely occur before the block below runs.
NSArray* notes = [[NSArray alloc] initWithArray:notesToPost];
// Post the notifications (or call the delegate method) on the main thread.
dispatch_async(dispatch_get_main_queue(), ^{
for (NSString* note in notes)
{
[self->_delegate VDKQueue:self receivedNotification:note forPath:fpath];
if (!self->_delegate || self->_alwaysPostNotifications)
{
[NSNotificationCenter.defaultCenter postNotificationName:note object:self userInfo:@{ @"path" : fpath }];
}
}
});
}
// Close our kqueue's file descriptor
if (close(theFD) == -1)
{
NSLog(@"VDKQueue watcherThread: Couldn't close main kqueue (%d)", errno);
}
#if DEBUG_LOG_THREAD_LIFETIME
NSLog(@"watcherThread finished.");
#endif
}
#pragma mark -
#pragma mark PUBLIC METHODS
- (void)addPath:(NSString*)aPath
{
[self addPath:aPath notifyingAbout:VDKQueueNotifyDefault];
}
- (void)addPath:(NSString*)aPath notifyingAbout:(u_int)flags
{
if (!aPath)
{
return;
}
@synchronized(self)
{
if (_watchedPathEntries[aPath])
{
// Only add this path if we don't already have it.
return;
}
VDKQueuePathEntry* entry = [self addPathToQueue:aPath notifyingAbout:flags];
if (!entry)
{
// By default, a darwin process can only have 256 file descriptors open at once.
// https://wilsonmar.github.io/maximum-limits/
NSLog(
@"VDKQueue tried to add the path %@ to watchedPathEntries, but the VDKQueuePathEntry was nil. \nIt's possible that the host process has hit its max open file descriptors limit.",
aPath);
}
}
}
- (void)removePath:(NSString*)aPath
{
if (!aPath)
{
return;
}
@synchronized(self)
{
VDKQueuePathEntry* entry = _watchedPathEntries[aPath];
// Remove it only if we're watching it.
if (entry)
{
[_watchedPathEntries removeObjectForKey:aPath];
}
}
}
- (void)removeAllPaths
{
@synchronized(self)
{
[_watchedPathEntries removeAllObjects];
}
}
@end