/*
* Copyright (c) 2008-2025 Jonathan Schleifer <js@nil.im>
*
* 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
* <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include "unistd_wrapper.h"
#include <float.h>
#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 <OFReadyForReadingObserving>)object
{
[self of_updateObject: object
fileDescriptor: [object fileDescriptorForReading]
addTypes: kCFSocketReadCallBack
removeTypes: 0];
[super addObjectForReading: object];
}
- (void)addObjectForWriting: (id <OFReadyForWritingObserving>)object
{
if (![object isKindOfClass: [OFDatagramSocket class]])
[self of_updateObject: object
fileDescriptor: [object fileDescriptorForWriting]
addTypes: kCFSocketWriteCallBack
removeTypes: 0];
[super addObjectForWriting: object];
}
- (void)removeObjectForReading: (id <OFReadyForReadingObserving>)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