forked from daniel-pedersen/SKQueue
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathSFSMonitor.swift
More file actions
284 lines (243 loc) · 12.3 KB
/
SFSMonitor.swift
File metadata and controls
284 lines (243 loc) · 12.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
//
// SFSMonitor.swift
// Forked from https://github.com/daniel-pedersen/SKQueue
// Updated from kevents to Dispatch Source by using Apple's Directory Monitor
// See https://stackoverflow.com/a/61035069/10327858
//
// Created by Ron Regev on 18/05/2020.
// Copyright © 2020 Ron Regev. All rights reserved.
//
import Foundation
/// A protocol that allows delegates of `SFSMonitor` to respond to changes in a directory or of a specific file.
public protocol SFSMonitorDelegate {
func receivedNotification(_ notification: SFSMonitorNotification, url: URL, queue: SFSMonitor)
}
/// A string representation of possible changes detected by SFSMonitor.
public enum SFSMonitorNotificationString: String {
case Rename
case Write
case Delete
case AttributeChange
case SizeIncrease
case LinkCountChange
case AccessRevocation
case Unlock
case DataAvailable
}
/// An OptionSet of possible changes detected by SFSMonitor.
public struct SFSMonitorNotification: OptionSet {
public let rawValue: UInt32
public init(rawValue: UInt32) {
self.rawValue = rawValue
}
public static let None = SFSMonitorNotification([])
public static let Rename = SFSMonitorNotification(rawValue: UInt32(NOTE_RENAME))
public static let Write = SFSMonitorNotification(rawValue: UInt32(NOTE_WRITE))
public static let Delete = SFSMonitorNotification(rawValue: UInt32(NOTE_DELETE))
public static let AttributeChange = SFSMonitorNotification(rawValue: UInt32(NOTE_ATTRIB))
public static let SizeIncrease = SFSMonitorNotification(rawValue: UInt32(NOTE_EXTEND))
public static let LinkCountChange = SFSMonitorNotification(rawValue: UInt32(NOTE_LINK))
public static let AccessRevocation = SFSMonitorNotification(rawValue: UInt32(NOTE_REVOKE))
public static let Unlock = SFSMonitorNotification(rawValue: UInt32(NOTE_FUNLOCK))
public static let DataAvailable = SFSMonitorNotification(rawValue: UInt32(NOTE_NONE))
public static let Default = SFSMonitorNotification(rawValue: UInt32(INT_MAX))
/// A method to convert the SFSMonitor OptionSet to String.
public func toStrings() -> [SFSMonitorNotificationString] {
var s = [SFSMonitorNotificationString]()
if contains(.Rename) { s.append(.Rename) }
if contains(.Write) { s.append(.Write) }
if contains(.Delete) { s.append(.Delete) }
if contains(.AttributeChange) { s.append(.AttributeChange) }
if contains(.SizeIncrease) { s.append(.SizeIncrease) }
if contains(.LinkCountChange) { s.append(.LinkCountChange) }
if contains(.AccessRevocation) { s.append(.AccessRevocation) }
if contains(.Unlock) { s.append(.Unlock) }
if contains(.DataAvailable) { s.append(.DataAvailable) }
return s
}
}
public class SFSMonitor {
// MARK: Properties
// The maximal number of file descriptors allowed to be opened. On iOS and iPadOS it is recommended to be kept at 224 or under (allowing 32 more for the app).
private static var maxMonitored : Int = 224
// A dictionary of SFSMonitor watched URLs and their Dispatch Sources for all class instances.
private static var watchedURLs : [URL : DispatchSource] = [:]
// Define the DispatchQueue
private let SFSMonitorQueue = DispatchQueue(label: "sfsmonitor", attributes: .concurrent)
// DispatchQueue for thread safety when modifying the watchedURLs array
private let SFSThreadSafetyQueue = DispatchQueue(label: "sfsthreadqueue", qos: .utility)
public var delegate: SFSMonitorDelegate?
// MARK: Initializers
public init?(delegate: SFSMonitorDelegate? = nil) {
self.delegate = delegate
}
// Note: if deinit is used to release the resources, they will be released unexpectedly. You have to call removeAllURLs() manually to do that.
// MARK: Add URL to the queue
/// Add a URL to the queue of files and folders monitored by SFSMonitor. Return values: 0 for success, 1 if the URL is already monitored, 2 if maximum number of monitored files and directories is reached, 3 for general error.
public func addURL(_ url: URL, notifyingAbout notification: SFSMonitorNotification = SFSMonitorNotification.Default) -> Int {
// Dispatch Semaphore for coordinating access to the watched URLs array
let watchedURLsSemaphore = DispatchSemaphore(value: 0)
// Check if the URL is not empty or inaccessible
do {
if !(try url.checkResourceIsReachable()) {
print ("SFSMonitor error: added URL is inaccessible: \(url)")
return 3
}
} catch {
print ("SFSMonitor error: added URL is inaccessible: \(url)")
return 3
}
// The next 2 tests have to read the watchedURLs array. To make this thread-safe,
// this must be done from our thread-safety dispatch queue.
// To be able to get the return values from the queue, we will use an internal
// function with a completion handler. The Dispatch Semaphore will ensure
// that we do not move on before these tests are complete.
// Variable that records the return values of the tests
var initialTestsValue : Int = 0
// Internal function that performs the tests
func initialTests (returnValue: (Int) -> ()) {
// Make the reads thread-safe
self.SFSThreadSafetyQueue.sync {
// Check if this URL is not already present
if SFSMonitor.watchedURLs.keys.contains(url) {
print ("SFSMonitor error: trying to add an already monitored URL to queue: \(url)")
returnValue(1)
watchedURLsSemaphore.signal()
return
}
// Check if the number of open file descriptors exceeds the limit
if SFSMonitor.watchedURLs.count >= SFSMonitor.maxMonitored {
print ("SFSMonitor error: number of allowed file descriptors exceeded")
returnValue(2)
watchedURLsSemaphore.signal()
return
}
// If we got here, the return value is 0
returnValue(0)
watchedURLsSemaphore.signal()
}
}
// Call the internal function to perform the tests
initialTests { returnValue in
initialTestsValue = returnValue
}
// Wait until we get the results back
watchedURLsSemaphore.wait()
// With anything other than 0, return the value
if initialTestsValue != 0 {
return initialTestsValue
}
// Open the file or directory referenced by URL for monitoring only.
let fileDescriptor = open(FileManager.default.fileSystemRepresentation(withPath: url.path), O_EVTONLY)
guard fileDescriptor >= 0 else {
print ("SFSMonitor error: could not create a file descriptor for URL: \(url)")
return 3
}
// Define a dispatch source monitoring the file or directory for additions, deletions, and renamings.
if let SFSMonitorSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor, eventMask: DispatchSource.FileSystemEvent.all, queue: SFSMonitorQueue) as? DispatchSource {
// Define the block to call when a file change is detected.
SFSMonitorSource.setEventHandler {
// Call out to the `SFSMonitorDelegate` so that it can react appropriately to the change.
let event = SFSMonitorSource.data as DispatchSource.FileSystemEvent
let notification = SFSMonitorNotification(rawValue: UInt32(event.rawValue))
self.delegate?.receivedNotification(notification, url: url, queue: self)
}
// Define a cancel handler to ensure the directory is closed when the source is cancelled.
SFSMonitorSource.setCancelHandler {
close(fileDescriptor)
self.SFSThreadSafetyQueue.async(flags: .barrier) {
SFSMonitor.watchedURLs.removeValue(forKey: url)
}
}
// Start monitoring
SFSMonitorSource.resume()
// Populate our watched URL array within the thread-safe queue
self.SFSThreadSafetyQueue.async(flags: .barrier) {
SFSMonitor.watchedURLs[url] = SFSMonitorSource
}
} else {
print ("SFSMonitor error: could not create a Dispatch Source for URL: \(url)")
return 3
}
return 0
}
/// A boolean value that indicates whether the entered URL is already being monitored by SFSMonitor.
public func isURLWatched(_ url: URL) -> Bool {
// This query has to be done through an internal function with a completion handler
// (with the help of a semaphore) so that we can use the dispatch queue for thread protection
let isURLWatchedSemaphore = DispatchSemaphore(value: 0)
func isURLWatchedInternalFunction(completion: (Bool) -> ()) {
self.SFSThreadSafetyQueue.sync {
completion(SFSMonitor.watchedURLs.keys.contains(url))
isURLWatchedSemaphore.signal()
}
}
var returnValue = false
isURLWatchedInternalFunction {completion in
returnValue = completion
}
isURLWatchedSemaphore.wait()
return returnValue
}
/// Remove URL from the SFSMonitor queue and close its file reference.
public func removeURL(_ url: URL) {
SFSThreadSafetyQueue.sync {
if let SFSMonitorSource = SFSMonitor.watchedURLs[url] {
// Cancel dispatch source and remove it from list
SFSMonitorSource.cancel()
}
}
}
/// Reset the SFSMonitor queue.
public func removeAllURLs() {
SFSThreadSafetyQueue.sync {
for watchedUrl in SFSMonitor.watchedURLs {
watchedUrl.value.cancel()
}
}
}
/// The number of URLs being watched by SFSMonitor.
public func numberOfWatchedURLs() -> Int {
// This query has to be done through an internal function with a completion handler
// (with the help of a semaphore) so that we can use the dispatch queue for thread protection
let numberOfWatchedURLsSemaphore = DispatchSemaphore(value: 0)
func numberOfWatchedURLsInternal(completion: (Int) -> ()) {
self.SFSThreadSafetyQueue.sync {
completion(SFSMonitor.watchedURLs.count)
numberOfWatchedURLsSemaphore.signal()
}
}
var returnValue : Int = 0
numberOfWatchedURLsInternal { completion in
returnValue = completion
}
numberOfWatchedURLsSemaphore.wait()
return returnValue
}
/// An array of all URLs being watched by SFSMonitor.
public func URLsWatched() -> [URL] {
// This query has to be done through an internal function with a completion handler
// (with the help of a semaphore) so that we can use the dispatch queue for thread protection
let URLsWatchedSemaphore = DispatchSemaphore(value: 0)
func URLsWatchedInternal(completion: ([URL]) -> ()) {
self.SFSThreadSafetyQueue.sync {
completion(Array(SFSMonitor.watchedURLs.keys))
URLsWatchedSemaphore.signal()
}
}
var returnValue : [URL] = []
URLsWatchedInternal { completion in
returnValue = completion
}
URLsWatchedSemaphore.wait()
return returnValue
}
/// Set the maximal number of file descriptors allowed to be opened. On iOS and iPadOS it is recommended to be kept at 224 or under (allowing 32 more for the app).
public func setMaxMonitored(number: Int) {
SFSMonitor.maxMonitored = number
}
/// Get the current maximal number of file descriptors allowed to be opened.
public func getMaxMonitored() -> Int {
return SFSMonitor.maxMonitored
}
}