├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Package.swift ├── README.md └── SKQueue.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 5 | 6 | ## [1.2.0] - 2018-09-27 7 | ### Added 8 | - Swift 4 support. 9 | - `Unlock` event notification, for files being unlocked by the `funlock` syscall. 10 | - `DataAvailable` event notification, to test for `EVFILT_READ` activation. 11 | - This changelog. 12 | 13 | ### Removed 14 | - Logging to the system console. 15 | 16 | ## [1.1.0] - 2017-04-25 17 | ### Added 18 | - Method `fileDescriptorForPath` in `SKQueue`. 19 | - Optional `delegate` parameter to the `SKQueue` initializer. 20 | 21 | ## [1.0.0] - 2017-04-10 22 | ### Changed 23 | - API follows the [Swift API Design Guidelines](https://swift.org/documentation/api-design-guidelines/). 24 | 25 | ### Removed 26 | - An overloaded `receivedNotification` in the `SKQueueDelegate` protocol which accepts notification as a string. 27 | 28 | ## [0.9.0] - 2017-04-10 29 | ### Added 30 | - Swift package manager support. 31 | 32 | [1.2.0]: https://github.com/daniel-pedersen/SKQueue/tree/v1.2.0 33 | [1.1.0]: https://github.com/daniel-pedersen/SKQueue/tree/v1.1.0 34 | [1.0.0]: https://github.com/daniel-pedersen/SKQueue/tree/v1.0.0 35 | [0.9.0]: https://github.com/daniel-pedersen/SKQueue/tree/v0.9.0 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Daniel Pedersen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "SKQueue", 7 | products: [ 8 | .library(name: "SKQueue", targets: ["SKQueue"]) 9 | ], 10 | dependencies: [], 11 | targets: [ 12 | .target(name: "SKQueue", path: ".", sources: ["SKQueue.swift"]) 13 | ] 14 | ) 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SKQueue 2 | SKQueue is a Swift libary used to monitor changes to the filesystem. 3 | It wraps the part of the kernel event notification interface of libc, [kqueue](https://en.wikipedia.org/wiki/Kqueue). 4 | This means SKQueue has a very small footprint and is highly scalable, just like kqueue. 5 | 6 | ## Requirements 7 | * Swift tools version 4 8 | 9 | To build in older environments just replace `Package.swift` with [this file](https://github.com/daniel-pedersen/SKQueue/blob/v1.1.0/Package.swift). 10 | 11 | ## Installation 12 | 13 | ### Swift Package Manager 14 | To use SKQueue, add the code below to your `dependencies` in `Package.swift`. 15 | Then run `swift package fetch` to fetch SKQueue. 16 | ```swift 17 | .package(url: "https://github.com/daniel-pedersen/SKQueue.git", from: "1.2.0"), 18 | ``` 19 | 20 | ## Usage 21 | To monitor the filesystem with `SKQueue`, you first need a `SKQueueDelegate` instance that can accept notifications. 22 | Paths to watch can then be added with `addPath`, as per the example below. 23 | 24 | ### Example 25 | ```swift 26 | import SKQueue 27 | 28 | class SomeClass: SKQueueDelegate { 29 | func receivedNotification(_ notification: SKQueueNotification, path: String, queue: SKQueue) { 30 | print("\(notification.toStrings().map { $0.rawValue }) @ \(path)") 31 | } 32 | } 33 | 34 | let delegate = SomeClass() 35 | let queue = SKQueue(delegate: delegate)! 36 | 37 | queue.addPath("/Users/steve/Documents") 38 | queue.addPath("/Users/steve/Documents/dog.jpg") 39 | ``` 40 | | Action | Sample output | 41 | |:---------------------------------------------------:|:-------------------------------------------------------------:| 42 | | Add or remove file in `/Users/steve/Documents` | `["Write"] @ /Users/steve/Documents` | 43 | | Add or remove directory in `/Users/steve/Documents` | `["Write", "SizeIncrease"] @ /Users/steve/Documents` | 44 | | Write to file `/Users/steve/Documents/dog.jpg` | `["Rename", "SizeIncrease"] @ /Users/steve/Documents/dog.jpg` | 45 | 46 | ## Contributing 47 | 48 | 1. Fork it! 49 | 2. Create your feature branch: `git checkout -b my-new-feature` 50 | 3. Commit your changes: `git commit -am 'Add some feature'` 51 | 4. Push to the branch: `git push origin my-new-feature` 52 | 5. Submit a pull request :D 53 | -------------------------------------------------------------------------------- /SKQueue.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol SKQueueDelegate { 4 | func receivedNotification(_ notification: SKQueueNotification, path: String, queue: SKQueue) 5 | } 6 | 7 | public enum SKQueueNotificationString: String { 8 | case Rename 9 | case Write 10 | case Delete 11 | case AttributeChange 12 | case SizeIncrease 13 | case LinkCountChange 14 | case AccessRevocation 15 | case Unlock 16 | case DataAvailable 17 | } 18 | 19 | public struct SKQueueNotification: OptionSet { 20 | public let rawValue: UInt32 21 | 22 | public init(rawValue: UInt32) { 23 | self.rawValue = rawValue 24 | } 25 | 26 | public static let None = SKQueueNotification(rawValue: 0) 27 | public static let Rename = SKQueueNotification(rawValue: UInt32(NOTE_RENAME)) 28 | public static let Write = SKQueueNotification(rawValue: UInt32(NOTE_WRITE)) 29 | public static let Delete = SKQueueNotification(rawValue: UInt32(NOTE_DELETE)) 30 | public static let AttributeChange = SKQueueNotification(rawValue: UInt32(NOTE_ATTRIB)) 31 | public static let SizeIncrease = SKQueueNotification(rawValue: UInt32(NOTE_EXTEND)) 32 | public static let LinkCountChange = SKQueueNotification(rawValue: UInt32(NOTE_LINK)) 33 | public static let AccessRevocation = SKQueueNotification(rawValue: UInt32(NOTE_REVOKE)) 34 | public static let Unlock = SKQueueNotification(rawValue: UInt32(NOTE_FUNLOCK)) 35 | public static let DataAvailable = SKQueueNotification(rawValue: UInt32(NOTE_NONE)) 36 | public static let Default = SKQueueNotification(rawValue: UInt32(INT_MAX)) 37 | 38 | public func toStrings() -> [SKQueueNotificationString] { 39 | var s = [SKQueueNotificationString]() 40 | if contains(.Rename) { s.append(.Rename) } 41 | if contains(.Write) { s.append(.Write) } 42 | if contains(.Delete) { s.append(.Delete) } 43 | if contains(.AttributeChange) { s.append(.AttributeChange) } 44 | if contains(.SizeIncrease) { s.append(.SizeIncrease) } 45 | if contains(.LinkCountChange) { s.append(.LinkCountChange) } 46 | if contains(.AccessRevocation) { s.append(.AccessRevocation) } 47 | if contains(.Unlock) { s.append(.Unlock) } 48 | if contains(.DataAvailable) { s.append(.DataAvailable) } 49 | return s 50 | } 51 | } 52 | 53 | public class SKQueue { 54 | private let kqueueId: Int32 55 | private var watchedPaths = [String: Int32]() 56 | private var keepWatcherThreadRunning = false 57 | public var delegate: SKQueueDelegate? 58 | 59 | public init?(delegate: SKQueueDelegate? = nil) { 60 | kqueueId = kqueue() 61 | if kqueueId == -1 { 62 | return nil 63 | } 64 | self.delegate = delegate 65 | } 66 | 67 | deinit { 68 | keepWatcherThreadRunning = false 69 | removeAllPaths() 70 | close(kqueueId) 71 | } 72 | 73 | public func addPath(_ path: String, notifyingAbout notification: SKQueueNotification = SKQueueNotification.Default) { 74 | var fileDescriptor: Int32! = watchedPaths[path] 75 | if fileDescriptor == nil { 76 | fileDescriptor = open(FileManager.default.fileSystemRepresentation(withPath: path), O_EVTONLY) 77 | guard fileDescriptor >= 0 else { return } 78 | watchedPaths[path] = fileDescriptor 79 | } 80 | 81 | var edit = kevent( 82 | ident: UInt(fileDescriptor), 83 | filter: Int16(EVFILT_VNODE), 84 | flags: UInt16(EV_ADD | EV_CLEAR), 85 | fflags: notification.rawValue, 86 | data: 0, 87 | udata: nil 88 | ) 89 | kevent(kqueueId, &edit, 1, nil, 0, nil) 90 | 91 | if !keepWatcherThreadRunning { 92 | keepWatcherThreadRunning = true 93 | DispatchQueue.global().async(execute: watcherThread) 94 | } 95 | } 96 | 97 | private func watcherThread() { 98 | var event = kevent() 99 | var timeout = timespec(tv_sec: 1, tv_nsec: 0) 100 | while (keepWatcherThreadRunning) { 101 | if kevent(kqueueId, nil, 0, &event, 1, &timeout) > 0 && event.filter == EVFILT_VNODE && event.fflags > 0 { 102 | guard let (path, _) = watchedPaths.first(where: { $1 == event.ident }) else { continue } 103 | let notification = SKQueueNotification(rawValue: event.fflags) 104 | DispatchQueue.global().async { 105 | self.delegate?.receivedNotification(notification, path: path, queue: self) 106 | } 107 | } 108 | } 109 | } 110 | 111 | public func isPathWatched(_ path: String) -> Bool { 112 | return watchedPaths[path] != nil 113 | } 114 | 115 | public func removePath(_ path: String) { 116 | if let fileDescriptor = watchedPaths.removeValue(forKey: path) { 117 | close(fileDescriptor) 118 | } 119 | } 120 | 121 | public func removeAllPaths() { 122 | watchedPaths.keys.forEach(removePath) 123 | } 124 | 125 | public func numberOfWatchedPaths() -> Int { 126 | return watchedPaths.count 127 | } 128 | 129 | public func fileDescriptorForPath(_ path: String) -> Int32 { 130 | if let fileDescriptor = watchedPaths[path] { 131 | return fcntl(fileDescriptor, F_DUPFD) 132 | } 133 | return -1 134 | } 135 | } 136 | --------------------------------------------------------------------------------