/* * Copyright (c) 2008-2025 Jonathan Schleifer * * All rights reserved. * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3.0 only, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * version 3.0 for more details. * * You should have received a copy of the GNU Lesser General Public License * version 3.0 along with this program. If not, see * . */ #include "config.h" #include "unistd_wrapper.h" #include #import "OFCFRunLoopKernelEventObserver.h" #import "OFDatagramSocket.h" #import "OFMapTable.h" #import "OFPair.h" #import "OFRunLoop.h" #import "OFInitializationFailedException.h" #import "OFObserveKernelEventsFailedException.h" extern id objc_retain(id object); extern void objc_release(id object); @interface OFKernelEventObserver (CFRunLoop) @end struct MapTableEntry { CFSocketRef socket; CFRunLoopSourceRef source; CFOptionFlags types; }; static void * retainObject(void *object) { return [(id)object retain]; } static void releaseObject(void *object) { [(id)object release]; } static void freeMapTableEntry(void *object) { struct MapTableEntry *entry = object; if (entry->source != NULL) { CFRunLoopSourceInvalidate(entry->source); CFRelease(entry->source); } if (entry->socket != NULL) { CFSocketInvalidate(entry->socket); CFRelease(entry->socket); } free(entry); } static OFMapTableFunctions objectFunctions = { .retain = retainObject, .release = releaseObject }; static OFMapTableFunctions mapTableEntryFunctions = { .release = freeMapTableEntry }; #ifdef __clang__ # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wunknown-pragmas" # pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation" #endif @implementation OFKernelEventObserver (CFRunLoop) + (instancetype)alloc { if (self == [OFKernelEventObserver class]) return [OFCFRunLoopKernelEventObserver alloc]; return [super alloc]; } + (bool)handlesForeignEvents { return true; } @end #ifdef __clang__ # pragma clang diagnostic pop #endif @implementation OFCFRunLoopKernelEventObserver static void callback(CFSocketRef sock, CFSocketCallBackType type, CFDataRef address, const void *data, void *info_) { void *pool = objc_autoreleasePoolPush(); OFPair *info = info_; id object; OFCFRunLoopKernelEventObserver *observer; OFAssert(info != nil); object = info.firstObject; observer = info.secondObject; if (object == nil) { char buffer; OFAssert(sock == observer->_cancelSocket); OFAssert(type == kCFSocketReadCallBack); OFEnsure(read(observer->_cancelFD[0], &buffer, 1) == 1); return; } if (type & kCFSocketReadCallBack) [observer->_delegate objectIsReadyForReading: object]; if (type & kCFSocketWriteCallBack) [observer->_delegate objectIsReadyForWriting: object]; objc_autoreleasePoolPop(pool); } + (unsigned int)of_createID OF_DIRECT { unsigned int ID; @synchronized (self) { static unsigned int currentID = 0; ID = currentID++; } return ID; } - (instancetype)initWithRunLoopMode: (OFRunLoopMode)mode { self = [super initWithRunLoopMode: mode]; @try { void *pool = objc_autoreleasePoolPush(); CFSocketContext context = { .version = 0, .info = [OFPair pairWithFirstObject: nil secondObject: self], .retain = (const void *(*)(const void *))retainObject, .release = (void (*)(const void *))releaseObject }; CFOptionFlags flags; _runLoop = (CFRunLoopRef)CFRetain(CFRunLoopGetCurrent()); if ([mode isEqual: OFDefaultRunLoopMode]) _runLoopMode = CFRetain(kCFRunLoopDefaultMode); else _runLoopMode = CFStringCreateWithFormat( kCFAllocatorDefault, NULL, CFSTR("OFCFRunLoopKernelEventObserver_%u"), [OFCFRunLoopKernelEventObserver of_createID]); if (_runLoopMode == NULL) @throw [OFInitializationFailedException exceptionWithClass: self.class]; _mapTable = [[OFMapTable alloc] initWithKeyFunctions: objectFunctions objectFunctions: mapTableEntryFunctions]; _cancelSocket = CFSocketCreateWithNative(kCFAllocatorDefault, _cancelFD[0], kCFSocketReadCallBack, callback, &context); if (_cancelSocket == NULL) @throw [OFInitializationFailedException exceptionWithClass: self.class]; flags = CFSocketGetSocketFlags(_cancelSocket); flags &= ~kCFSocketCloseOnInvalidate; CFSocketSetSocketFlags(_cancelSocket, flags); _cancelSource = CFSocketCreateRunLoopSource( kCFAllocatorDefault, _cancelSocket, 0); if (_cancelSource == NULL) @throw [OFInitializationFailedException exceptionWithClass: self.class]; CFRunLoopAddSource(_runLoop, _cancelSource, _runLoopMode); objc_autoreleasePoolPop(pool); } @catch (id e) { [self release]; @throw e; } return self; } - (void)dealloc { if (_cancelSource != NULL) { CFRunLoopSourceInvalidate(_cancelSource); CFRelease(_cancelSource); } if (_cancelSocket != NULL) { CFSocketInvalidate(_cancelSocket); CFRelease(_cancelSocket); } if (_runLoop != NULL) CFRelease(_runLoop); if (_runLoopMode != NULL) CFRelease(_runLoopMode); [_mapTable release]; [super dealloc]; } - (void)of_updateObject: (id)object fileDescriptor: (int)fd addTypes: (CFOptionFlags)addTypes removeTypes: (CFOptionFlags)removeTypes OF_DIRECT { /* * This method destroys the old CFSocket and CFRunLoopSource and creates * new ones. While this might sound inefficient, there is unfortunately * no other way, as using CFSocket{Enable,Disable}CallBacks from a * callback does not work as expected. */ void *pool = objc_autoreleasePoolPush(); CFSocketContext context = { .version = 0, }; CFOptionFlags types = 0; struct MapTableEntry *oldEntry, *newEntry; if ((oldEntry = [_mapTable objectForKey: object]) != NULL) types = oldEntry->types; types = (types | addTypes) & ~removeTypes; if (types == 0) { [_mapTable removeObjectForKey: object]; objc_autoreleasePoolPop(pool); return; } newEntry = OFAllocZeroedMemory(1, sizeof(*newEntry)); @try { CFOptionFlags flags; context.info = [OFPair pairWithFirstObject: object secondObject: self]; context.retain = (const void *(*)(const void *))retainObject; context.release = (void (*)(const void *))releaseObject; if ((newEntry->socket = CFSocketCreateWithNative( kCFAllocatorDefault, fd, types, callback, &context)) == NULL) @throw [OFObserveKernelEventsFailedException exceptionWithObserver: self errNo: 0]; flags = CFSocketGetSocketFlags(newEntry->socket); flags &= ~kCFSocketCloseOnInvalidate; CFSocketSetSocketFlags(newEntry->socket, flags); if ((newEntry->source = CFSocketCreateRunLoopSource( kCFAllocatorDefault, newEntry->socket, 0)) == NULL) @throw [OFObserveKernelEventsFailedException exceptionWithObserver: self errNo: 0]; CFRunLoopAddSource(_runLoop, newEntry->source, _runLoopMode); newEntry->types = types; [_mapTable setObject: newEntry forKey: object]; } @catch (id e) { freeMapTableEntry(newEntry); } objc_autoreleasePoolPop(pool); } - (void)addObjectForReading: (id )object { [self of_updateObject: object fileDescriptor: [object fileDescriptorForReading] addTypes: kCFSocketReadCallBack removeTypes: 0]; [super addObjectForReading: object]; } - (void)addObjectForWriting: (id )object { if (![object isKindOfClass: [OFDatagramSocket class]]) [self of_updateObject: object fileDescriptor: [object fileDescriptorForWriting] addTypes: kCFSocketWriteCallBack removeTypes: 0]; [super addObjectForWriting: object]; } - (void)removeObjectForReading: (id )object { [self of_updateObject: object fileDescriptor: [object fileDescriptorForReading] addTypes: 0 removeTypes: kCFSocketReadCallBack]; [super removeObjectForReading: object]; } - (void)removeObjectForWriting: (id< OFReadyForWritingObserving>)object { if (![object isKindOfClass: [OFDatagramSocket class]]) [self of_updateObject: object fileDescriptor: [object fileDescriptorForWriting] addTypes: 0 removeTypes: kCFSocketWriteCallBack]; [super removeObjectForWriting: object]; } - (void)observeForTimeInterval: (OFTimeInterval)timeInterval { if ([self processReadBuffers]) return; /* * It seems CFRunLoop never fires for an UDP socket ready for writing, * so instead always manually fire all UDP sockets that are being * observed as ready for writing. */ for (id object in [[_writeObjects copy] autorelease]) if ([object isKindOfClass: [OFDatagramSocket class]]) [_delegate objectIsReadyForWriting: object]; if (timeInterval == -1) /* There is no value for infinite, so make it really long. */ timeInterval = DBL_MAX; CFRunLoopRunInMode(_runLoopMode, timeInterval, true); } @end