1 /**
2 	`CFRunLoop` based event loop for macOS UI compatible operation.
3 */
4 module eventcore.drivers.posix.cfrunloop;
5 @safe: /*@nogc:*/ nothrow:
6 
7 version (EventcoreCFRunLoopDriver):
8 
9 import eventcore.drivers.posix.kqueue;
10 import eventcore.internal.corefoundation;
11 import eventcore.internal.utils;
12 import core.time;
13 
14 
15 alias CFRunLoopEventDriver = PosixEventDriver!CFRunLoopEventLoop;
16 
17 final class CFRunLoopEventLoop : KqueueEventLoopBase {
18 @safe nothrow:
19 	private {
20 		CFFileDescriptorRef m_kqueueDescriptor;
21 		CFRunLoopSourceRef m_kqueueSource;
22 	}
23 
24 	this()
25 	@trusted @nogc {
26 		super();
27 
28 		CFFileDescriptorContext ctx;
29 		ctx.info = cast(void*)this;
30 
31 		m_kqueueDescriptor = CFFileDescriptorCreate(kCFAllocatorDefault,
32 			m_queue, false, &processKqueue, &ctx);
33 
34 		m_kqueueSource = CFFileDescriptorCreateRunLoopSource(kCFAllocatorDefault, m_kqueueDescriptor, 0);
35 		CFRunLoopAddSource(CFRunLoopGetCurrent(), m_kqueueSource, kCFRunLoopDefaultMode);
36 	}
37 
38 	override bool doProcessEvents(Duration timeout)
39 	@trusted {
40 		import std.algorithm.comparison : min;
41 
42 		CFFileDescriptorEnableCallBacks(m_kqueueDescriptor, CFOptionFlags.kCFFileDescriptorReadCallBack);
43 
44 		// submit changes and process pending events
45 		auto kres = doProcessEventsBase(0.seconds);
46 		if (kres) timeout = 0.seconds;
47 
48 		// NOTE: the timeout per CFRunLoopRunInMode call is limited to five
49 		//       seconds to work around the issue that the kqueue CFFileDescriptor
50 		//       sometimes does not fire. There seems to be some kind of race-
51 		//       condition, between the edge-triggered kqueue events and
52 		//       CFFileDescriptorEnableCallBacks/CFRunLoopRunInMode.
53 		//
54 		//       Even changing the order of calls in processKqueue to first
55 		//       re-enable the callbacks and *then* process the already pending
56 		//       events does not help (and is also explicitly discouraged in
57 		//       Apple's documentation).
58 		//
59 		// NOTE: In contrast to Apple's documentation, kCFRunLoopRunStopped is
60 		//       returned not only if the loop is explicitly stopped, but also
61 		//       after handling events normally. For this reason we treat any
62 		//       return value other than "timed out" as having processed an event.
63 		if (timeout == 0.seconds) {
64 			while (true) {
65 				auto res = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true);
66 				if (res == CFRunLoopRunResult.kCFRunLoopRunTimedOut)
67 					break;
68 				else kres = true;
69 			}
70 
71 			CFFileDescriptorEnableCallBacks(m_kqueueDescriptor, CFOptionFlags.kCFFileDescriptorReadCallBack);
72 			kres |= doProcessEventsBase(0.seconds);
73 		} else while (timeout > 0.seconds) {
74 			auto tol = min(timeout, 5.seconds);
75 			timeout -= tol;
76 			CFTimeInterval to = 1e-7 * tol.total!"hnsecs";
77 			auto res = CFRunLoopRunInMode(kCFRunLoopDefaultMode, to, true);
78 			if (res != CFRunLoopRunResult.kCFRunLoopRunTimedOut) {
79 				kres = true;
80 				break;
81 			}
82 
83 			CFFileDescriptorEnableCallBacks(m_kqueueDescriptor, CFOptionFlags.kCFFileDescriptorReadCallBack);
84 			kres |= doProcessEventsBase(0.seconds);
85 			if (kres) break;
86 		}
87 
88 		return kres;
89 	}
90 
91 	override void dispose()
92 	{
93 		() @trusted {
94 			CFRelease(m_kqueueSource);
95 			CFRelease(m_kqueueDescriptor);
96 		} ();
97 		super.dispose();
98 	}
99 
100 	private static extern(C) void processKqueue(CFFileDescriptorRef fdref,
101 		CFOptionFlags callBackTypes, void* info)
102 	{
103 		auto this_ = () @trusted { return cast(CFRunLoopEventLoop)info; } ();
104 		this_.doProcessEventsBase(0.seconds);
105 	}
106 }