16
16
import CNIODarwin
17
17
import CNIOLinux
18
18
import NIOConcurrencyHelpers
19
+ import NIOCore
19
20
import NIOPosix
20
21
@preconcurrency import SystemPackage
21
22
@@ -89,17 +90,17 @@ extension DirectoryEntries {
89
90
public typealias AsyncIterator = BatchedIterator
90
91
public typealias Element = [ DirectoryEntry ]
91
92
92
- private let stream : BufferedOrAnyStream < [ DirectoryEntry ] >
93
+ private let stream : BufferedOrAnyStream < [ DirectoryEntry ] , DirectoryEntryProducer >
93
94
94
95
/// Creates a ``DirectoryEntries/Batched`` sequence by wrapping an `AsyncSequence`
95
96
/// of directory entry batches.
96
97
public init < S: AsyncSequence > ( wrapping sequence: S ) where S. Element == Element {
97
- self . stream = BufferedOrAnyStream ( wrapping: sequence)
98
+ self . stream = BufferedOrAnyStream < [ DirectoryEntry ] , DirectoryEntryProducer > ( wrapping: sequence)
98
99
}
99
100
100
101
fileprivate init ( handle: SystemFileHandle , recursive: Bool ) {
101
102
// Expanding the batches yields watermarks of 256 and 512 directory entries.
102
- let stream = BufferedStream . makeBatchedDirectoryEntryStream (
103
+ let stream = NIOThrowingAsyncSequenceProducer . makeBatchedDirectoryEntryStream (
103
104
handle: handle,
104
105
recursive: recursive,
105
106
entriesPerBatch: 64 ,
@@ -116,9 +117,11 @@ extension DirectoryEntries {
116
117
117
118
/// An `AsyncIteratorProtocol` of `Array<DirectoryEntry>`.
118
119
public struct BatchedIterator : AsyncIteratorProtocol {
119
- private var iterator : BufferedOrAnyStream < [ DirectoryEntry ] > . AsyncIterator
120
+ private var iterator : BufferedOrAnyStream < [ DirectoryEntry ] , DirectoryEntryProducer > . AsyncIterator
120
121
121
- init ( wrapping iterator: BufferedOrAnyStream < [ DirectoryEntry ] > . AsyncIterator ) {
122
+ fileprivate init (
123
+ wrapping iterator: BufferedOrAnyStream < [ DirectoryEntry ] , DirectoryEntryProducer > . AsyncIterator
124
+ ) {
122
125
self . iterator = iterator
123
126
}
124
127
@@ -135,52 +138,95 @@ extension DirectoryEntries.Batched.AsyncIterator: Sendable {}
135
138
// MARK: - Internal
136
139
137
140
@available ( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * )
138
- extension BufferedStream where Element == [ DirectoryEntry ] {
141
+ extension NIOThrowingAsyncSequenceProducer
142
+ where
143
+ Element == [ DirectoryEntry ] ,
144
+ Failure == ( any Error ) ,
145
+ Strategy == NIOAsyncSequenceProducerBackPressureStrategies . HighLowWatermark ,
146
+ Delegate == DirectoryEntryProducer
147
+ {
139
148
fileprivate static func makeBatchedDirectoryEntryStream(
140
149
handle: SystemFileHandle ,
141
150
recursive: Bool ,
142
151
entriesPerBatch: Int ,
143
152
lowWatermark: Int ,
144
153
highWatermark: Int
145
- ) -> BufferedStream < [ DirectoryEntry ] > {
146
- let state = DirectoryEnumerator ( handle: handle, recursive: recursive)
147
- let protectedState = NIOLockedValueBox ( state)
148
-
149
- var ( stream, source) = BufferedStream . makeStream (
150
- of: [ DirectoryEntry ] . self,
151
- backPressureStrategy: . watermark( low: lowWatermark, high: highWatermark)
152
- )
153
-
154
- source. onTermination = {
155
- guard let threadPool = protectedState. withLockedValue ( { $0. threadPoolForClosing ( ) } ) else {
156
- return
157
- }
158
-
159
- threadPool. submit { _ in // always run, even if cancelled
160
- protectedState. withLockedValue { state in
161
- state. closeIfNecessary ( )
162
- }
163
- }
164
- }
165
-
154
+ ) -> NIOThrowingAsyncSequenceProducer <
155
+ [ DirectoryEntry ] , any Error , NIOAsyncSequenceProducerBackPressureStrategies . HighLowWatermark ,
156
+ DirectoryEntryProducer
157
+ > {
166
158
let producer = DirectoryEntryProducer (
167
- state : protectedState ,
168
- source : source ,
159
+ handle : handle ,
160
+ recursive : recursive ,
169
161
entriesPerBatch: entriesPerBatch
170
162
)
171
- // Start producing immediately.
172
- producer. produceMore ( )
173
163
174
- return stream
164
+ let nioThrowingAsyncSequence = NIOThrowingAsyncSequenceProducer . makeSequence (
165
+ elementType: [ DirectoryEntry ] . self,
166
+ backPressureStrategy: NIOAsyncSequenceProducerBackPressureStrategies . HighLowWatermark (
167
+ lowWatermark: lowWatermark,
168
+ highWatermark: highWatermark
169
+ ) ,
170
+ finishOnDeinit: false ,
171
+ delegate: producer
172
+ )
173
+
174
+ producer. setSequenceProducerSource ( nioThrowingAsyncSequence. source)
175
+
176
+ return nioThrowingAsyncSequence. sequence
175
177
}
176
178
}
177
179
178
180
@available ( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * )
179
- private struct DirectoryEntryProducer {
181
+ private typealias DirectoryEntrySequenceProducer = NIOThrowingAsyncSequenceProducer <
182
+ [ DirectoryEntry ] , Error , NIOAsyncSequenceProducerBackPressureStrategies . HighLowWatermark , DirectoryEntryProducer
183
+ >
184
+
185
+ @available ( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * )
186
+ private final class DirectoryEntryProducer : NIOAsyncSequenceProducerDelegate {
180
187
let state : NIOLockedValueBox < DirectoryEnumerator >
181
- let source : BufferedStream < [ DirectoryEntry ] > . Source
182
188
let entriesPerBatch : Int
183
189
190
+ init ( handle: SystemFileHandle , recursive: Bool , entriesPerBatch: Int ) {
191
+ let state = DirectoryEnumerator ( handle: handle, recursive: recursive)
192
+ self . state = NIOLockedValueBox ( state)
193
+ self . entriesPerBatch = entriesPerBatch
194
+ }
195
+
196
+ func didTerminate( ) {
197
+ guard let threadPool = self . state. withLockedValue ( { $0. threadPoolForClosing ( ) } ) else {
198
+ return
199
+ }
200
+
201
+ threadPool. submit { _ in // always run, even if cancelled
202
+ self . state. withLockedValue { state in
203
+ state. closeIfNecessary ( )
204
+ }
205
+ }
206
+ }
207
+
208
+ /// sets the source within the producer state
209
+ func setSequenceProducerSource( _ sequenceProducerSource: DirectoryEntrySequenceProducer . Source ) {
210
+ self . state. withLockedValue { state in
211
+ switch state. state {
212
+ case . idle:
213
+ state. sequenceProducerSource = sequenceProducerSource
214
+ case . done:
215
+ sequenceProducerSource. finish ( )
216
+ case . open, . openPausedProducing:
217
+ fatalError ( )
218
+ case . modifying:
219
+ fatalError ( )
220
+ }
221
+ }
222
+ }
223
+
224
+ func clearSource( ) {
225
+ self . state. withLockedValue { state in
226
+ state. sequenceProducerSource = nil
227
+ }
228
+ }
229
+
184
230
/// The 'entry point' for producing elements.
185
231
///
186
232
/// Calling this function will start producing directory entries asynchronously by dispatching
@@ -207,6 +253,12 @@ private struct DirectoryEntryProducer {
207
253
}
208
254
}
209
255
256
+ func pauseProducing( ) {
257
+ self . state. withLockedValue { state in
258
+ state. pauseProducing ( )
259
+ }
260
+ }
261
+
210
262
private func nextBatch( ) throws -> [ DirectoryEntry ] {
211
263
try self . state. withLockedValue { state in
212
264
try state. next ( self . entriesPerBatch)
@@ -221,45 +273,51 @@ private struct DirectoryEntryProducer {
221
273
// Failed to read more entries: close and notify the stream so consumers receive the
222
274
// error.
223
275
self . close ( )
224
- self . source. finish ( throwing: error)
276
+ let source = self . state. withLockedValue { state in
277
+ state. sequenceProducerSource
278
+ }
279
+ source? . finish ( error)
280
+ self . clearSource ( )
225
281
}
226
282
}
227
283
228
284
private func onNextBatch( _ entries: [ DirectoryEntry ] ) {
285
+ let source = self . state. withLockedValue { state in
286
+ state. sequenceProducerSource
287
+ }
288
+
289
+ guard let source else {
290
+ assertionFailure ( " unexpectedly missing source " )
291
+ return
292
+ }
293
+
229
294
// No entries were read: this must be the end (as the batch size must be greater than zero).
230
295
if entries. isEmpty {
231
- self . source. finish ( throwing: nil )
296
+ source. finish ( )
297
+ self . clearSource ( )
232
298
return
233
299
}
234
300
235
301
// Reading short means reading EOF. The enumerator closes itself in that case.
236
302
let readEOF = entries. count < self . entriesPerBatch
237
303
238
304
// Entries were produced: yield them and maybe produce more.
239
- do {
240
- let writeResult = try self . source. write ( contentsOf: CollectionOfOne ( entries) )
241
- // Exit early if EOF was read; no use in trying to produce more.
242
- if readEOF {
243
- self . source. finish ( throwing: nil )
244
- return
245
- }
305
+ let writeResult = source. yield ( contentsOf: CollectionOfOne ( entries) )
246
306
247
- switch writeResult {
248
- case . produceMore:
249
- self . produceMore ( )
250
- case let . enqueueCallback( token) :
251
- self . source. enqueueCallback ( callbackToken: token) {
252
- switch $0 {
253
- case . success:
254
- self . produceMore ( )
255
- case . failure:
256
- self . close ( )
257
- }
258
- }
259
- }
260
- } catch {
261
- // Failure to write means the source is already done, that's okay we just need to
262
- // update our state and stop producing.
307
+ // Exit early if EOF was read; no use in trying to produce more.
308
+ if readEOF {
309
+ source. finish ( )
310
+ self . clearSource ( )
311
+ return
312
+ }
313
+
314
+ switch writeResult {
315
+ case . produceMore:
316
+ self . produceMore ( )
317
+ case . stopProducing:
318
+ self . pauseProducing ( )
319
+ case . dropped:
320
+ // The source is finished; mark ourselves as done.
263
321
self . close ( )
264
322
}
265
323
}
@@ -282,25 +340,30 @@ private struct DirectoryEntryProducer {
282
340
/// Note that this is not a `Sequence` because we allow for errors to be thrown on `next()`.
283
341
@available ( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * )
284
342
private struct DirectoryEnumerator : Sendable {
285
- private enum State : @unchecked Sendable {
343
+ internal enum State : @unchecked Sendable {
286
344
case modifying
287
345
case idle( SystemFileHandle . SendableView , recursive: Bool )
288
346
case open( NIOThreadPool , Source , [ DirectoryEntry ] )
347
+ case openPausedProducing( NIOThreadPool , Source , [ DirectoryEntry ] )
289
348
case done
290
349
}
291
350
292
351
/// The source of directory entries.
293
- private enum Source {
352
+ internal enum Source {
294
353
case readdir( CInterop . DirPointer )
295
354
case fts( CInterop . FTSPointer )
296
355
}
297
356
298
357
/// The current state of enumeration.
299
- private var state : State
358
+ internal var state : State
300
359
301
360
/// The path to the directory being enumerated.
302
361
private let path : FilePath
303
362
363
+ /// The route via which directory entry batches are yielded,
364
+ /// the sourcing end of the `DirectoryEntrySequenceProducer`
365
+ internal var sequenceProducerSource : DirectoryEntrySequenceProducer . Source ?
366
+
304
367
/// Information about an entry returned by FTS. See 'fts(3)'.
305
368
private enum FTSInfo : Hashable , Sendable {
306
369
case directoryPreOrder
@@ -353,22 +416,38 @@ private struct DirectoryEnumerator: Sendable {
353
416
self . path = handle. path
354
417
}
355
418
356
- internal func produceMore( ) -> NIOThreadPool ? {
419
+ internal mutating func produceMore( ) -> NIOThreadPool ? {
357
420
switch self . state {
358
421
case let . idle( handle, _) :
359
422
return handle. threadPool
360
423
case let . open( threadPool, _, _) :
361
424
return threadPool
425
+ case . openPausedProducing( let threadPool, let source, let array) :
426
+ self . state = . open( threadPool, source, array)
427
+ return threadPool
362
428
case . done:
363
429
return nil
364
430
case . modifying:
365
431
fatalError ( )
366
432
}
367
433
}
368
434
435
+ internal mutating func pauseProducing( ) {
436
+ switch self . state {
437
+ case . open( let threadPool, let source, let array) :
438
+ self . state = . openPausedProducing( threadPool, source, array)
439
+ case . idle:
440
+ ( ) // we won't apply back pressure until something has been read
441
+ case . openPausedProducing, . done:
442
+ ( ) // no-op
443
+ case . modifying:
444
+ fatalError ( )
445
+ }
446
+ }
447
+
369
448
internal func threadPoolForClosing( ) -> NIOThreadPool ? {
370
449
switch self . state {
371
- case let . open ( threadPool, _, _) :
450
+ case . open ( let threadPool , _ , _ ) , . openPausedProducing ( let threadPool, _, _) :
372
451
return threadPool
373
452
case . idle, . done:
374
453
// Don't need to close in the idle state: we don't own the handle.
@@ -397,7 +476,7 @@ private struct DirectoryEnumerator: Sendable {
397
476
// We don't own the handle so don't close it.
398
477
self . state = . done
399
478
400
- case let . open ( _, mode, _) :
479
+ case . open ( _ , let mode , _ ) , . openPausedProducing ( _, let mode, _) :
401
480
self . state = . done
402
481
switch mode {
403
482
case . readdir( let dir) :
@@ -631,6 +710,9 @@ private struct DirectoryEnumerator: Sendable {
631
710
return result
632
711
}
633
712
713
+ case . openPausedProducing:
714
+ return . yield( . success( [ ] ) )
715
+
634
716
case . done:
635
717
return . yield( . success( [ ] ) )
636
718
0 commit comments