mirror of
https://github.com/gnustep/libs-base.git
synced 2025-05-11 08:40:44 +00:00
Various tweaks.
git-svn-id: svn+ssh://svn.gna.org/svn/gnustep/libs/base/trunk@12123 72102866-910b-0410-8b05-ffd578937521
This commit is contained in:
parent
0e2bd154dc
commit
22cb892946
4 changed files with 468 additions and 411 deletions
10
ChangeLog
10
ChangeLog
|
@ -1,3 +1,13 @@
|
||||||
|
2002-01-16 Richard Frith-Macdonald <rfm@gnu.org>
|
||||||
|
|
||||||
|
* Source/NSRunLoop.m: Wrap code in exception handlers to reset current
|
||||||
|
runloop mode if an exception occurs ... may remove this again if the
|
||||||
|
performance is too bad.
|
||||||
|
Use initialiser for NSTimer to avoid having to put timers into the
|
||||||
|
autorelease pool.
|
||||||
|
* Headers/Foundation/NSTimer.h: Expose GNUstep initialiser since
|
||||||
|
OpenStep and MacOS-X don't have one.
|
||||||
|
|
||||||
Wed Jan 16 13:46:24 2002 Nicola Pero <nicola@brainstorm.co.uk>
|
Wed Jan 16 13:46:24 2002 Nicola Pero <nicola@brainstorm.co.uk>
|
||||||
|
|
||||||
Fixed dynamical loading of frameworks.
|
Fixed dynamical loading of frameworks.
|
||||||
|
|
|
@ -40,9 +40,9 @@
|
||||||
BOOL _invalidated; /* Must be 2nd - for NSRunLoop optimisation */
|
BOOL _invalidated; /* Must be 2nd - for NSRunLoop optimisation */
|
||||||
BOOL _repeats;
|
BOOL _repeats;
|
||||||
NSTimeInterval _interval;
|
NSTimeInterval _interval;
|
||||||
id _target;
|
id _target;
|
||||||
SEL _selector;
|
SEL _selector;
|
||||||
id _info;
|
id _info;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Creating timer objects. */
|
/* Creating timer objects. */
|
||||||
|
@ -51,18 +51,18 @@
|
||||||
invocation: (NSInvocation*)invocation
|
invocation: (NSInvocation*)invocation
|
||||||
repeats: (BOOL)f;
|
repeats: (BOOL)f;
|
||||||
+ (NSTimer*) scheduledTimerWithTimeInterval: (NSTimeInterval)ti
|
+ (NSTimer*) scheduledTimerWithTimeInterval: (NSTimeInterval)ti
|
||||||
target: object
|
target: (id)object
|
||||||
selector: (SEL)selector
|
selector: (SEL)selector
|
||||||
userInfo: info
|
userInfo: (id)info
|
||||||
repeats: (BOOL)f;
|
repeats: (BOOL)f;
|
||||||
|
|
||||||
+ (NSTimer*) timerWithTimeInterval: (NSTimeInterval)ti
|
+ (NSTimer*) timerWithTimeInterval: (NSTimeInterval)ti
|
||||||
invocation: (NSInvocation*)invocation
|
invocation: (NSInvocation*)invocation
|
||||||
repeats: (BOOL)f;
|
repeats: (BOOL)f;
|
||||||
+ (NSTimer*) timerWithTimeInterval: (NSTimeInterval)ti
|
+ (NSTimer*) timerWithTimeInterval: (NSTimeInterval)ti
|
||||||
target: object
|
target: (id)object
|
||||||
selector: (SEL)selector
|
selector: (SEL)selector
|
||||||
userInfo: info
|
userInfo: (id)info
|
||||||
repeats: (BOOL)f;
|
repeats: (BOOL)f;
|
||||||
|
|
||||||
- (void) fire;
|
- (void) fire;
|
||||||
|
@ -76,6 +76,13 @@
|
||||||
- (NSDate*) fireDate;
|
- (NSDate*) fireDate;
|
||||||
- (id) userInfo;
|
- (id) userInfo;
|
||||||
|
|
||||||
|
#ifndef NO_GNUSTEP
|
||||||
|
- (id) initWithTimeInterval: (NSTimeInterval)ti
|
||||||
|
targetOrInvocation: (id)object
|
||||||
|
selector: (SEL)selector
|
||||||
|
userInfo: (id)info
|
||||||
|
repeats: (BOOL)f;
|
||||||
|
#endif
|
||||||
@end
|
@end
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -244,8 +244,8 @@ static NSComparisonResult aSort(GSIArrayItem i0, GSIArrayItem i1)
|
||||||
@end
|
@end
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The GSTimedPerformer class is used to hold information about
|
* The GSTimedPerformer class is used to hold information about
|
||||||
* messages which are due to be sent to objects at a particular time.
|
* messages which are due to be sent to objects at a particular time.
|
||||||
*/
|
*/
|
||||||
@interface GSTimedPerformer: NSObject <GCFinalization>
|
@interface GSTimedPerformer: NSObject <GCFinalization>
|
||||||
{
|
{
|
||||||
|
@ -268,6 +268,7 @@ static NSComparisonResult aSort(GSIArrayItem i0, GSIArrayItem i1)
|
||||||
- (void) dealloc
|
- (void) dealloc
|
||||||
{
|
{
|
||||||
[self gcFinalize];
|
[self gcFinalize];
|
||||||
|
TEST_RELEASE(timer);
|
||||||
RELEASE(target);
|
RELEASE(target);
|
||||||
RELEASE(argument);
|
RELEASE(argument);
|
||||||
[super dealloc];
|
[super dealloc];
|
||||||
|
@ -275,7 +276,7 @@ static NSComparisonResult aSort(GSIArrayItem i0, GSIArrayItem i1)
|
||||||
|
|
||||||
- (void) fire
|
- (void) fire
|
||||||
{
|
{
|
||||||
timer = nil;
|
DESTROY(timer);
|
||||||
[target performSelector: selector withObject: argument];
|
[target performSelector: selector withObject: argument];
|
||||||
[[[NSRunLoop currentRunLoop] _timedPerformers]
|
[[[NSRunLoop currentRunLoop] _timedPerformers]
|
||||||
removeObjectIdenticalTo: self];
|
removeObjectIdenticalTo: self];
|
||||||
|
@ -284,7 +285,9 @@ static NSComparisonResult aSort(GSIArrayItem i0, GSIArrayItem i1)
|
||||||
- (void) gcFinalize
|
- (void) gcFinalize
|
||||||
{
|
{
|
||||||
if (timer != nil)
|
if (timer != nil)
|
||||||
[timer invalidate];
|
{
|
||||||
|
[timer invalidate];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (id) initWithSelector: (SEL)aSelector
|
- (id) initWithSelector: (SEL)aSelector
|
||||||
|
@ -293,16 +296,17 @@ static NSComparisonResult aSort(GSIArrayItem i0, GSIArrayItem i1)
|
||||||
delay: (NSTimeInterval)delay
|
delay: (NSTimeInterval)delay
|
||||||
{
|
{
|
||||||
self = [super init];
|
self = [super init];
|
||||||
if (self)
|
if (self != nil)
|
||||||
{
|
{
|
||||||
selector = aSelector;
|
selector = aSelector;
|
||||||
target = RETAIN(aTarget);
|
target = RETAIN(aTarget);
|
||||||
argument = RETAIN(anArgument);
|
argument = RETAIN(anArgument);
|
||||||
timer = [NSTimer timerWithTimeInterval: delay
|
timer = [[NSTimer allocWithZone: NSDefaultMallocZone()]
|
||||||
target: self
|
initWithTimeInterval: delay
|
||||||
selector: @selector(fire)
|
targetOrInvocation: self
|
||||||
userInfo: nil
|
selector: @selector(fire)
|
||||||
repeats: NO];
|
userInfo: nil
|
||||||
|
repeats: NO];
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
@ -830,20 +834,23 @@ const NSMapTableValueCallBacks ArrayMapValueCallBacks =
|
||||||
/* This is the designated initializer. */
|
/* This is the designated initializer. */
|
||||||
- (id) init
|
- (id) init
|
||||||
{
|
{
|
||||||
[super init];
|
self = [super init];
|
||||||
_mode_2_timers = NSCreateMapTable (NSNonRetainedObjectMapKeyCallBacks,
|
if (self != nil)
|
||||||
ArrayMapValueCallBacks, 0);
|
{
|
||||||
_mode_2_watchers = NSCreateMapTable (NSObjectMapKeyCallBacks,
|
_mode_2_timers = NSCreateMapTable (NSNonRetainedObjectMapKeyCallBacks,
|
||||||
ArrayMapValueCallBacks, 0);
|
ArrayMapValueCallBacks, 0);
|
||||||
_mode_2_performers = NSCreateMapTable (NSObjectMapKeyCallBacks,
|
_mode_2_watchers = NSCreateMapTable (NSObjectMapKeyCallBacks,
|
||||||
ArrayMapValueCallBacks, 0);
|
ArrayMapValueCallBacks, 0);
|
||||||
_timedPerformers = [[NSMutableArray alloc] initWithCapacity: 8];
|
_mode_2_performers = NSCreateMapTable (NSObjectMapKeyCallBacks,
|
||||||
_efdMap = NSCreateMapTable (NSIntMapKeyCallBacks,
|
ArrayMapValueCallBacks, 0);
|
||||||
WatcherMapValueCallBacks, 0);
|
_timedPerformers = [[NSMutableArray alloc] initWithCapacity: 8];
|
||||||
_rfdMap = NSCreateMapTable (NSIntMapKeyCallBacks,
|
_efdMap = NSCreateMapTable (NSIntMapKeyCallBacks,
|
||||||
WatcherMapValueCallBacks, 0);
|
WatcherMapValueCallBacks, 0);
|
||||||
_wfdMap = NSCreateMapTable (NSIntMapKeyCallBacks,
|
_rfdMap = NSCreateMapTable (NSIntMapKeyCallBacks,
|
||||||
WatcherMapValueCallBacks, 0);
|
WatcherMapValueCallBacks, 0);
|
||||||
|
_wfdMap = NSCreateMapTable (NSIntMapKeyCallBacks,
|
||||||
|
WatcherMapValueCallBacks, 0);
|
||||||
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -894,9 +901,10 @@ const NSMapTableValueCallBacks ArrayMapValueCallBacks =
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Fire appropriate timers and determine the earliest time that anything
|
/**
|
||||||
watched for becomes useless. */
|
* Fire appropriate timers and determine the earliest time that anything
|
||||||
|
* watched for becomes useless.
|
||||||
|
*/
|
||||||
- (NSDate*) limitDateForMode: (NSString*)mode
|
- (NSDate*) limitDateForMode: (NSString*)mode
|
||||||
{
|
{
|
||||||
id saved_mode;
|
id saved_mode;
|
||||||
|
@ -910,116 +918,125 @@ const NSMapTableValueCallBacks ArrayMapValueCallBacks =
|
||||||
saved_mode = _current_mode;
|
saved_mode = _current_mode;
|
||||||
_current_mode = mode;
|
_current_mode = mode;
|
||||||
|
|
||||||
timers = NSMapGet(_mode_2_timers, mode);
|
NS_DURING
|
||||||
if (timers)
|
|
||||||
{
|
{
|
||||||
while (GSIArrayCount(timers) != 0)
|
timers = NSMapGet(_mode_2_timers, mode);
|
||||||
|
if (timers)
|
||||||
{
|
{
|
||||||
min_timer = GSIArrayItemAtIndex(timers, 0).obj;
|
while (GSIArrayCount(timers) != 0)
|
||||||
if (timerInvalidated(min_timer) == YES)
|
|
||||||
{
|
{
|
||||||
GSIArrayRemoveItemAtIndex(timers, 0);
|
min_timer = GSIArrayItemAtIndex(timers, 0).obj;
|
||||||
min_timer = nil;
|
if (timerInvalidated(min_timer) == YES)
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([timerDate(min_timer) timeIntervalSinceNow] > 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
GSIArrayRemoveItemAtIndexNoRelease(timers, 0);
|
|
||||||
/* Firing will also increment its fireDate, if it is repeating. */
|
|
||||||
[min_timer fire];
|
|
||||||
if (timerInvalidated(min_timer) == NO)
|
|
||||||
{
|
|
||||||
GSIArrayInsertSortedNoRetain(timers,
|
|
||||||
(GSIArrayItem)min_timer, aSort);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
RELEASE(min_timer);
|
|
||||||
}
|
|
||||||
min_timer = nil;
|
|
||||||
GSNotifyASAP(); /* Post notifications. */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Is this right? At the moment we invalidate and discard watchers
|
|
||||||
whose limit-dates have passed. */
|
|
||||||
watchers = NSMapGet(_mode_2_watchers, mode);
|
|
||||||
if (watchers)
|
|
||||||
{
|
|
||||||
while (GSIArrayCount(watchers) != 0)
|
|
||||||
{
|
|
||||||
min_watcher = GSIArrayItemAtIndex(watchers, 0).obj;
|
|
||||||
|
|
||||||
if (min_watcher->_invalidated == YES)
|
|
||||||
{
|
|
||||||
GSIArrayRemoveItemAtIndex(watchers, 0);
|
|
||||||
min_watcher = nil;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([min_watcher->_date timeIntervalSinceNow] > 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
id obj;
|
|
||||||
NSDate *nxt = nil;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* If the receiver or its delegate wants to know about
|
|
||||||
* timeouts - inform it and give it a chance to set a
|
|
||||||
* revised limit date.
|
|
||||||
*/
|
|
||||||
GSIArrayRemoveItemAtIndexNoRelease(watchers, 0);
|
|
||||||
obj = min_watcher->receiver;
|
|
||||||
if ([obj respondsToSelector:
|
|
||||||
@selector(timedOutEvent:type:forMode:)])
|
|
||||||
{
|
{
|
||||||
nxt = [obj timedOutEvent: min_watcher->data
|
GSIArrayRemoveItemAtIndex(timers, 0);
|
||||||
type: min_watcher->type
|
min_timer = nil;
|
||||||
forMode: _current_mode];
|
continue;
|
||||||
}
|
}
|
||||||
else if ([obj respondsToSelector: @selector(delegate)])
|
|
||||||
|
if ([timerDate(min_timer) timeIntervalSinceNow] > 0)
|
||||||
{
|
{
|
||||||
obj = [obj delegate];
|
break;
|
||||||
if (obj != nil && [obj respondsToSelector:
|
}
|
||||||
@selector(timedOutEvent:type:forMode:)])
|
|
||||||
|
GSIArrayRemoveItemAtIndexNoRelease(timers, 0);
|
||||||
|
/* Firing will also increment its fireDate, if it is repeating. */
|
||||||
|
[min_timer fire];
|
||||||
|
if (timerInvalidated(min_timer) == NO)
|
||||||
|
{
|
||||||
|
GSIArrayInsertSortedNoRetain(timers,
|
||||||
|
(GSIArrayItem)min_timer, aSort);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RELEASE(min_timer);
|
||||||
|
}
|
||||||
|
min_timer = nil;
|
||||||
|
GSNotifyASAP(); /* Post notifications. */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Is this right? At the moment we invalidate and discard watchers
|
||||||
|
whose limit-dates have passed. */
|
||||||
|
watchers = NSMapGet(_mode_2_watchers, mode);
|
||||||
|
if (watchers)
|
||||||
|
{
|
||||||
|
while (GSIArrayCount(watchers) != 0)
|
||||||
|
{
|
||||||
|
min_watcher = GSIArrayItemAtIndex(watchers, 0).obj;
|
||||||
|
|
||||||
|
if (min_watcher->_invalidated == YES)
|
||||||
|
{
|
||||||
|
GSIArrayRemoveItemAtIndex(watchers, 0);
|
||||||
|
min_watcher = nil;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([min_watcher->_date timeIntervalSinceNow] > 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
id obj;
|
||||||
|
NSDate *nxt = nil;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If the receiver or its delegate wants to know about
|
||||||
|
* timeouts - inform it and give it a chance to set a
|
||||||
|
* revised limit date.
|
||||||
|
*/
|
||||||
|
GSIArrayRemoveItemAtIndexNoRelease(watchers, 0);
|
||||||
|
obj = min_watcher->receiver;
|
||||||
|
if ([obj respondsToSelector:
|
||||||
|
@selector(timedOutEvent:type:forMode:)])
|
||||||
{
|
{
|
||||||
nxt = [obj timedOutEvent: min_watcher->data
|
nxt = [obj timedOutEvent: min_watcher->data
|
||||||
type: min_watcher->type
|
type: min_watcher->type
|
||||||
forMode: _current_mode];
|
forMode: _current_mode];
|
||||||
}
|
}
|
||||||
|
else if ([obj respondsToSelector: @selector(delegate)])
|
||||||
|
{
|
||||||
|
obj = [obj delegate];
|
||||||
|
if (obj != nil && [obj respondsToSelector:
|
||||||
|
@selector(timedOutEvent:type:forMode:)])
|
||||||
|
{
|
||||||
|
nxt = [obj timedOutEvent: min_watcher->data
|
||||||
|
type: min_watcher->type
|
||||||
|
forMode: _current_mode];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nxt && [nxt timeIntervalSinceNow] > 0.0)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* If the watcher has been given a revised limit date -
|
||||||
|
* re-insert it into the queue in the correct place.
|
||||||
|
*/
|
||||||
|
ASSIGN(min_watcher->_date, nxt);
|
||||||
|
GSIArrayInsertSortedNoRetain(watchers,
|
||||||
|
(GSIArrayItem)min_watcher, aSort);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* If the watcher is now useless - invalidate and
|
||||||
|
* release it.
|
||||||
|
*/
|
||||||
|
min_watcher->_invalidated = YES;
|
||||||
|
RELEASE(min_watcher);
|
||||||
|
}
|
||||||
|
min_watcher = nil;
|
||||||
}
|
}
|
||||||
if (nxt && [nxt timeIntervalSinceNow] > 0.0)
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
* If the watcher has been given a revised limit date -
|
|
||||||
* re-insert it into the queue in the correct place.
|
|
||||||
*/
|
|
||||||
ASSIGN(min_watcher->_date, nxt);
|
|
||||||
GSIArrayInsertSortedNoRetain(watchers,
|
|
||||||
(GSIArrayItem)min_watcher, aSort);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
* If the watcher is now useless - invalidate and
|
|
||||||
* release it.
|
|
||||||
*/
|
|
||||||
min_watcher->_invalidated = YES;
|
|
||||||
RELEASE(min_watcher);
|
|
||||||
}
|
|
||||||
min_watcher = nil;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_current_mode = saved_mode;
|
||||||
}
|
}
|
||||||
|
NS_HANDLER
|
||||||
|
{
|
||||||
|
_current_mode = saved_mode;
|
||||||
|
[localException raise];
|
||||||
|
}
|
||||||
|
NS_ENDHANDLER
|
||||||
|
|
||||||
_current_mode = saved_mode;
|
|
||||||
RELEASE(arp);
|
RELEASE(arp);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -1087,309 +1104,329 @@ const NSMapTableValueCallBacks ArrayMapValueCallBacks =
|
||||||
}
|
}
|
||||||
_current_mode = mode;
|
_current_mode = mode;
|
||||||
|
|
||||||
/* Find out how much time we should wait, and set SELECT_TIMEOUT. */
|
NS_DURING
|
||||||
if (!limit_date)
|
|
||||||
{
|
{
|
||||||
/* Don't wait at all. */
|
/* Find out how much time we should wait, and set SELECT_TIMEOUT. */
|
||||||
timeout.tv_sec = 0;
|
if (!limit_date)
|
||||||
timeout.tv_usec = 0;
|
|
||||||
select_timeout = &timeout;
|
|
||||||
}
|
|
||||||
else if ((ti = [limit_date timeIntervalSinceNow])
|
|
||||||
< LONG_MAX && ti > 0.0)
|
|
||||||
{
|
|
||||||
/* Wait until the LIMIT_DATE. */
|
|
||||||
if (debug_run_loop)
|
|
||||||
printf ("\tNSRunLoop accept input before %f (seconds from now %f)\n",
|
|
||||||
[limit_date timeIntervalSinceReferenceDate], ti);
|
|
||||||
/* If LIMIT_DATE has already past, return immediately. */
|
|
||||||
timeout.tv_sec = ti;
|
|
||||||
timeout.tv_usec = (ti - timeout.tv_sec) * 1000000.0;
|
|
||||||
select_timeout = &timeout;
|
|
||||||
}
|
|
||||||
else if (ti <= 0.0)
|
|
||||||
{
|
|
||||||
/* The LIMIT_DATE has already past; return immediately without
|
|
||||||
polling any inputs. */
|
|
||||||
GSCheckTasks();
|
|
||||||
[self _checkPerformers];
|
|
||||||
GSNotifyASAP();
|
|
||||||
if (debug_run_loop)
|
|
||||||
printf ("\tNSRunLoop limit date past, returning\n");
|
|
||||||
_current_mode = saved_mode;
|
|
||||||
RELEASE(arp);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
/* Wait forever. */
|
|
||||||
if (debug_run_loop)
|
|
||||||
printf ("\tNSRunLoop accept input waiting forever\n");
|
|
||||||
select_timeout = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Get ready to listen to file descriptors.
|
|
||||||
* Initialize the set of FDS we'll pass to select(), and make sure we
|
|
||||||
* have empty maps for keeping track of which watcher is associated
|
|
||||||
* with which file descriptor.
|
|
||||||
* The maps may not have been emptied if a previous call to this
|
|
||||||
* method was terminated by an exception.
|
|
||||||
*/
|
|
||||||
memset(&exception_fds, '\0', sizeof(exception_fds));
|
|
||||||
memset(&read_fds, '\0', sizeof(read_fds));
|
|
||||||
memset(&write_fds, '\0', sizeof(write_fds));
|
|
||||||
NSResetMapTable(_efdMap);
|
|
||||||
NSResetMapTable(_rfdMap);
|
|
||||||
NSResetMapTable(_wfdMap);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Do the pre-listening set-up for the file descriptors of this mode.
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
GSIArray watchers;
|
|
||||||
|
|
||||||
watchers = NSMapGet(_mode_2_watchers, mode);
|
|
||||||
if (watchers)
|
|
||||||
{
|
|
||||||
unsigned i = GSIArrayCount(watchers);
|
|
||||||
|
|
||||||
while (i-- > 0)
|
|
||||||
{
|
|
||||||
GSRunLoopWatcher *info;
|
|
||||||
int fd;
|
|
||||||
|
|
||||||
info = GSIArrayItemAtIndex(watchers, i).obj;
|
|
||||||
if (info->_invalidated == YES)
|
|
||||||
{
|
|
||||||
GSIArrayRemoveItemAtIndex(watchers, i);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
switch (info->type)
|
|
||||||
{
|
|
||||||
case ET_EDESC:
|
|
||||||
fd = (int)info->data;
|
|
||||||
if (fd > end_inputs)
|
|
||||||
end_inputs = fd;
|
|
||||||
FD_SET (fd, &exception_fds);
|
|
||||||
NSMapInsert(_efdMap, (void*)fd, info);
|
|
||||||
num_inputs++;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ET_RDESC:
|
|
||||||
fd = (int)info->data;
|
|
||||||
if (fd > end_inputs)
|
|
||||||
end_inputs = fd;
|
|
||||||
FD_SET (fd, &read_fds);
|
|
||||||
NSMapInsert(_rfdMap, (void*)fd, info);
|
|
||||||
num_inputs++;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ET_WDESC:
|
|
||||||
fd = (int)info->data;
|
|
||||||
if (fd > end_inputs)
|
|
||||||
end_inputs = fd;
|
|
||||||
FD_SET (fd, &write_fds);
|
|
||||||
NSMapInsert(_wfdMap, (void*)fd, info);
|
|
||||||
num_inputs++;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ET_RPORT:
|
|
||||||
if ([info->receiver isValid] == NO)
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
* We must remove an invalidated port.
|
|
||||||
*/
|
|
||||||
info->_invalidated = YES;
|
|
||||||
GSIArrayRemoveItemAtIndex(watchers, i);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
id port = info->receiver;
|
|
||||||
int port_fd_count = 128; // xxx #define this constant
|
|
||||||
int port_fd_array[port_fd_count];
|
|
||||||
|
|
||||||
if ([port respondsToSelector: @selector(getFds:count:)])
|
|
||||||
[port getFds: port_fd_array count: &port_fd_count];
|
|
||||||
if (debug_run_loop)
|
|
||||||
printf("\tNSRunLoop listening to %d sockets\n",
|
|
||||||
port_fd_count);
|
|
||||||
|
|
||||||
while (port_fd_count--)
|
|
||||||
{
|
|
||||||
fd = port_fd_array[port_fd_count];
|
|
||||||
FD_SET (port_fd_array[port_fd_count], &read_fds);
|
|
||||||
if (fd > end_inputs)
|
|
||||||
end_inputs = fd;
|
|
||||||
NSMapInsert(_rfdMap,
|
|
||||||
(void*)port_fd_array[port_fd_count], info);
|
|
||||||
num_inputs++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end_inputs++;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* If there are notifications in the 'idle' queue, we try an instantaneous
|
|
||||||
* select so that, if there is no input pending, we can service the queue.
|
|
||||||
* Similarly, if a task has completed, we need to deliver it's notifications.
|
|
||||||
*/
|
|
||||||
if (GSCheckTasks() || GSNotifyMore())
|
|
||||||
{
|
|
||||||
timeout.tv_sec = 0;
|
|
||||||
timeout.tv_usec = 0;
|
|
||||||
select_timeout = &timeout;
|
|
||||||
select_return = select (end_inputs, &read_fds, &write_fds, &exception_fds,
|
|
||||||
select_timeout);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
select_return = select (end_inputs, &read_fds, &write_fds, &exception_fds,
|
|
||||||
select_timeout);
|
|
||||||
|
|
||||||
if (debug_run_loop)
|
|
||||||
printf ("\tNSRunLoop select returned %d\n", select_return);
|
|
||||||
|
|
||||||
if (select_return < 0)
|
|
||||||
{
|
|
||||||
if (errno == EINTR)
|
|
||||||
{
|
{
|
||||||
GSCheckTasks();
|
/* Don't wait at all. */
|
||||||
select_return = 0;
|
timeout.tv_sec = 0;
|
||||||
|
timeout.tv_usec = 0;
|
||||||
|
select_timeout = &timeout;
|
||||||
|
}
|
||||||
|
else if ((ti = [limit_date timeIntervalSinceNow])
|
||||||
|
< LONG_MAX && ti > 0.0)
|
||||||
|
{
|
||||||
|
/* Wait until the LIMIT_DATE. */
|
||||||
|
if (debug_run_loop)
|
||||||
|
printf ("\tNSRunLoop accept input before %f (sec from now %f)\n",
|
||||||
|
[limit_date timeIntervalSinceReferenceDate], ti);
|
||||||
|
/* If LIMIT_DATE has already past, return immediately. */
|
||||||
|
timeout.tv_sec = ti;
|
||||||
|
timeout.tv_usec = (ti - timeout.tv_sec) * 1000000.0;
|
||||||
|
select_timeout = &timeout;
|
||||||
|
}
|
||||||
|
else if (ti <= 0.0)
|
||||||
|
{
|
||||||
|
/* The LIMIT_DATE has already past; return immediately without
|
||||||
|
polling any inputs. */
|
||||||
|
GSCheckTasks();
|
||||||
|
[self _checkPerformers];
|
||||||
|
GSNotifyASAP();
|
||||||
|
if (debug_run_loop)
|
||||||
|
printf ("\tNSRunLoop limit date past, returning\n");
|
||||||
|
_current_mode = saved_mode;
|
||||||
|
RELEASE(arp);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
#ifdef __MINGW__
|
|
||||||
else if (errno == 0)
|
|
||||||
{
|
|
||||||
/* MinGW often returns an errno == 0. Not sure why */
|
|
||||||
select_return = 0;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
/* Some exceptional condition happened. */
|
/* Wait forever. */
|
||||||
/* xxx We can do something with exception_fds, instead of
|
if (debug_run_loop)
|
||||||
aborting here. */
|
printf ("\tNSRunLoop accept input waiting forever\n");
|
||||||
NSLog (@"select() error in -acceptInputForMode:beforeDate: '%s'",
|
select_timeout = NULL;
|
||||||
GSLastErrorStr(errno));
|
|
||||||
abort ();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (select_return == 0)
|
/*
|
||||||
{
|
* Get ready to listen to file descriptors.
|
||||||
|
* Initialize the set of FDS we'll pass to select(), and make sure we
|
||||||
|
* have empty maps for keeping track of which watcher is associated
|
||||||
|
* with which file descriptor.
|
||||||
|
* The maps may not have been emptied if a previous call to this
|
||||||
|
* method was terminated by an exception.
|
||||||
|
*/
|
||||||
|
memset(&exception_fds, '\0', sizeof(exception_fds));
|
||||||
|
memset(&read_fds, '\0', sizeof(read_fds));
|
||||||
|
memset(&write_fds, '\0', sizeof(write_fds));
|
||||||
NSResetMapTable(_efdMap);
|
NSResetMapTable(_efdMap);
|
||||||
NSResetMapTable(_rfdMap);
|
NSResetMapTable(_rfdMap);
|
||||||
NSResetMapTable(_wfdMap);
|
NSResetMapTable(_wfdMap);
|
||||||
GSNotifyIdle();
|
|
||||||
[self _checkPerformers];
|
|
||||||
_current_mode = saved_mode;
|
|
||||||
RELEASE(arp);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Look at all the file descriptors select() says are ready for action;
|
|
||||||
* notify the corresponding object for each of the ready fd's.
|
|
||||||
* NB. It is possible for a watcher to be missing from the map - if
|
|
||||||
* the event handler of a previous watcher has 'run' the loop again
|
|
||||||
* before returning.
|
|
||||||
* NB. Each time this roop is entered, the starting position (_fdStart)
|
|
||||||
* is incremented - this is to ensure a fair distribtion over all
|
|
||||||
* inputs where multiple inputs are in use. Note - _fdStart can be
|
|
||||||
* modified while we are in the loop (by recursive calls).
|
|
||||||
*/
|
|
||||||
if (_fdStart >= end_inputs)
|
|
||||||
{
|
|
||||||
_fdStart = 0;
|
|
||||||
fdIndex = 0;
|
|
||||||
fdEnd = 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_fdStart++;
|
|
||||||
fdIndex = _fdStart;
|
|
||||||
fdEnd = _fdStart;
|
|
||||||
}
|
|
||||||
do
|
|
||||||
{
|
|
||||||
BOOL found = NO;
|
|
||||||
|
|
||||||
if (FD_ISSET (fdIndex, &exception_fds))
|
/*
|
||||||
{
|
* Do the pre-listening set-up for the file descriptors of this mode.
|
||||||
GSRunLoopWatcher *watcher;
|
*/
|
||||||
|
{
|
||||||
|
GSIArray watchers;
|
||||||
|
|
||||||
watcher = (GSRunLoopWatcher*)NSMapGet(_efdMap, (void*)fdIndex);
|
watchers = NSMapGet(_mode_2_watchers, mode);
|
||||||
if (watcher != nil && watcher->_invalidated == NO)
|
if (watchers)
|
||||||
|
{
|
||||||
|
unsigned i = GSIArrayCount(watchers);
|
||||||
|
|
||||||
|
while (i-- > 0)
|
||||||
|
{
|
||||||
|
GSRunLoopWatcher *info;
|
||||||
|
int fd;
|
||||||
|
|
||||||
|
info = GSIArrayItemAtIndex(watchers, i).obj;
|
||||||
|
if (info->_invalidated == YES)
|
||||||
|
{
|
||||||
|
GSIArrayRemoveItemAtIndex(watchers, i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (info->type)
|
||||||
|
{
|
||||||
|
case ET_EDESC:
|
||||||
|
fd = (int)info->data;
|
||||||
|
if (fd > end_inputs)
|
||||||
|
end_inputs = fd;
|
||||||
|
FD_SET (fd, &exception_fds);
|
||||||
|
NSMapInsert(_efdMap, (void*)fd, info);
|
||||||
|
num_inputs++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ET_RDESC:
|
||||||
|
fd = (int)info->data;
|
||||||
|
if (fd > end_inputs)
|
||||||
|
end_inputs = fd;
|
||||||
|
FD_SET (fd, &read_fds);
|
||||||
|
NSMapInsert(_rfdMap, (void*)fd, info);
|
||||||
|
num_inputs++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ET_WDESC:
|
||||||
|
fd = (int)info->data;
|
||||||
|
if (fd > end_inputs)
|
||||||
|
end_inputs = fd;
|
||||||
|
FD_SET (fd, &write_fds);
|
||||||
|
NSMapInsert(_wfdMap, (void*)fd, info);
|
||||||
|
num_inputs++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ET_RPORT:
|
||||||
|
if ([info->receiver isValid] == NO)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* We must remove an invalidated port.
|
||||||
|
*/
|
||||||
|
info->_invalidated = YES;
|
||||||
|
GSIArrayRemoveItemAtIndex(watchers, i);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
id port = info->receiver;
|
||||||
|
int port_fd_count = 128; // xxx #define this constant
|
||||||
|
int port_fd_array[port_fd_count];
|
||||||
|
|
||||||
|
if ([port respondsToSelector:
|
||||||
|
@selector(getFds:count:)])
|
||||||
|
{
|
||||||
|
[port getFds: port_fd_array
|
||||||
|
count: &port_fd_count];
|
||||||
|
}
|
||||||
|
if (debug_run_loop)
|
||||||
|
{
|
||||||
|
printf("\tNSRunLoop listening to %d sockets\n",
|
||||||
|
port_fd_count);
|
||||||
|
}
|
||||||
|
while (port_fd_count--)
|
||||||
|
{
|
||||||
|
fd = port_fd_array[port_fd_count];
|
||||||
|
FD_SET (port_fd_array[port_fd_count], &read_fds);
|
||||||
|
if (fd > end_inputs)
|
||||||
|
{
|
||||||
|
end_inputs = fd;
|
||||||
|
}
|
||||||
|
NSMapInsert(_rfdMap,
|
||||||
|
(void*)port_fd_array[port_fd_count], info);
|
||||||
|
num_inputs++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end_inputs++;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If there are notifications in the 'idle' queue, we try an
|
||||||
|
* instantaneous select so that, if there is no input pending,
|
||||||
|
* we can service the queue. Similarly, if a task has completed,
|
||||||
|
* we need to deliver it's notifications.
|
||||||
|
*/
|
||||||
|
if (GSCheckTasks() || GSNotifyMore())
|
||||||
|
{
|
||||||
|
timeout.tv_sec = 0;
|
||||||
|
timeout.tv_usec = 0;
|
||||||
|
select_timeout = &timeout;
|
||||||
|
select_return = select (end_inputs, &read_fds, &write_fds,
|
||||||
|
&exception_fds, select_timeout);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
select_return = select (end_inputs, &read_fds, &write_fds,
|
||||||
|
&exception_fds, select_timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debug_run_loop)
|
||||||
|
{
|
||||||
|
printf ("\tNSRunLoop select returned %d\n", select_return);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (select_return < 0)
|
||||||
|
{
|
||||||
|
if (errno == EINTR)
|
||||||
{
|
{
|
||||||
/*
|
GSCheckTasks();
|
||||||
* The watcher is still valid - so call it's receivers
|
select_return = 0;
|
||||||
* event handling method.
|
|
||||||
*/
|
|
||||||
(*watcher->handleEvent)(watcher->receiver,
|
|
||||||
eventSel, watcher->data, watcher->type,
|
|
||||||
(void*)(gsaddr)fdIndex, _current_mode);
|
|
||||||
}
|
}
|
||||||
GSNotifyASAP();
|
#ifdef __MINGW__
|
||||||
found = YES;
|
else if (errno == 0)
|
||||||
}
|
|
||||||
if (FD_ISSET (fdIndex, &write_fds))
|
|
||||||
{
|
|
||||||
GSRunLoopWatcher *watcher;
|
|
||||||
|
|
||||||
watcher = NSMapGet(_wfdMap, (void*)fdIndex);
|
|
||||||
if (watcher != nil && watcher->_invalidated == NO)
|
|
||||||
{
|
{
|
||||||
/*
|
/* MinGW often returns an errno == 0. Not sure why */
|
||||||
* The watcher is still valid - so call it's receivers
|
select_return = 0;
|
||||||
* event handling method.
|
|
||||||
*/
|
|
||||||
(*watcher->handleEvent)(watcher->receiver,
|
|
||||||
eventSel, watcher->data, watcher->type,
|
|
||||||
(void*)(gsaddr)fdIndex, _current_mode);
|
|
||||||
}
|
}
|
||||||
GSNotifyASAP();
|
#endif
|
||||||
found = YES;
|
else
|
||||||
}
|
|
||||||
if (FD_ISSET (fdIndex, &read_fds))
|
|
||||||
{
|
|
||||||
GSRunLoopWatcher *watcher;
|
|
||||||
|
|
||||||
watcher = (GSRunLoopWatcher*)NSMapGet(_rfdMap, (void*)fdIndex);
|
|
||||||
if (watcher != nil && watcher->_invalidated == NO)
|
|
||||||
{
|
{
|
||||||
/*
|
/* Some exceptional condition happened. */
|
||||||
* The watcher is still valid - so call it's receivers
|
/* xxx We can do something with exception_fds, instead of
|
||||||
* event handling method.
|
aborting here. */
|
||||||
*/
|
NSLog (@"select() error in -acceptInputForMode:beforeDate: '%s'",
|
||||||
(*watcher->handleEvent)(watcher->receiver,
|
GSLastErrorStr(errno));
|
||||||
|
abort ();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (select_return == 0)
|
||||||
|
{
|
||||||
|
NSResetMapTable(_efdMap);
|
||||||
|
NSResetMapTable(_rfdMap);
|
||||||
|
NSResetMapTable(_wfdMap);
|
||||||
|
GSNotifyIdle();
|
||||||
|
[self _checkPerformers];
|
||||||
|
_current_mode = saved_mode;
|
||||||
|
RELEASE(arp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Look at all the file descriptors select() says are ready for action;
|
||||||
|
* notify the corresponding object for each of the ready fd's.
|
||||||
|
* NB. It is possible for a watcher to be missing from the map - if
|
||||||
|
* the event handler of a previous watcher has 'run' the loop again
|
||||||
|
* before returning.
|
||||||
|
* NB. Each time this roop is entered, the starting position (_fdStart)
|
||||||
|
* is incremented - this is to ensure a fair distribtion over all
|
||||||
|
* inputs where multiple inputs are in use. Note - _fdStart can be
|
||||||
|
* modified while we are in the loop (by recursive calls).
|
||||||
|
*/
|
||||||
|
if (_fdStart >= end_inputs)
|
||||||
|
{
|
||||||
|
_fdStart = 0;
|
||||||
|
fdIndex = 0;
|
||||||
|
fdEnd = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_fdStart++;
|
||||||
|
fdIndex = _fdStart;
|
||||||
|
fdEnd = _fdStart;
|
||||||
|
}
|
||||||
|
do
|
||||||
|
{
|
||||||
|
BOOL found = NO;
|
||||||
|
|
||||||
|
if (FD_ISSET (fdIndex, &exception_fds))
|
||||||
|
{
|
||||||
|
GSRunLoopWatcher *watcher;
|
||||||
|
|
||||||
|
watcher = (GSRunLoopWatcher*)NSMapGet(_efdMap, (void*)fdIndex);
|
||||||
|
if (watcher != nil && watcher->_invalidated == NO)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* The watcher is still valid - so call it's receivers
|
||||||
|
* event handling method.
|
||||||
|
*/
|
||||||
|
(*watcher->handleEvent)(watcher->receiver,
|
||||||
eventSel, watcher->data, watcher->type,
|
eventSel, watcher->data, watcher->type,
|
||||||
(void*)(gsaddr)fdIndex, _current_mode);
|
(void*)(gsaddr)fdIndex, _current_mode);
|
||||||
|
}
|
||||||
|
GSNotifyASAP();
|
||||||
|
found = YES;
|
||||||
|
}
|
||||||
|
if (FD_ISSET (fdIndex, &write_fds))
|
||||||
|
{
|
||||||
|
GSRunLoopWatcher *watcher;
|
||||||
|
|
||||||
|
watcher = NSMapGet(_wfdMap, (void*)fdIndex);
|
||||||
|
if (watcher != nil && watcher->_invalidated == NO)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* The watcher is still valid - so call it's receivers
|
||||||
|
* event handling method.
|
||||||
|
*/
|
||||||
|
(*watcher->handleEvent)(watcher->receiver,
|
||||||
|
eventSel, watcher->data, watcher->type,
|
||||||
|
(void*)(gsaddr)fdIndex, _current_mode);
|
||||||
|
}
|
||||||
|
GSNotifyASAP();
|
||||||
|
found = YES;
|
||||||
|
}
|
||||||
|
if (FD_ISSET (fdIndex, &read_fds))
|
||||||
|
{
|
||||||
|
GSRunLoopWatcher *watcher;
|
||||||
|
|
||||||
|
watcher = (GSRunLoopWatcher*)NSMapGet(_rfdMap, (void*)fdIndex);
|
||||||
|
if (watcher != nil && watcher->_invalidated == NO)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* The watcher is still valid - so call it's receivers
|
||||||
|
* event handling method.
|
||||||
|
*/
|
||||||
|
(*watcher->handleEvent)(watcher->receiver,
|
||||||
|
eventSel, watcher->data, watcher->type,
|
||||||
|
(void*)(gsaddr)fdIndex, _current_mode);
|
||||||
|
}
|
||||||
|
GSNotifyASAP();
|
||||||
|
found = YES;
|
||||||
|
}
|
||||||
|
if (found == YES && --select_return == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (++fdIndex >= end_inputs)
|
||||||
|
{
|
||||||
|
fdIndex = 0;
|
||||||
}
|
}
|
||||||
GSNotifyASAP();
|
|
||||||
found = YES;
|
|
||||||
}
|
|
||||||
if (found == YES && --select_return == 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (++fdIndex >= end_inputs)
|
|
||||||
{
|
|
||||||
fdIndex = 0;
|
|
||||||
}
|
}
|
||||||
|
while (fdIndex != fdEnd);
|
||||||
|
|
||||||
|
/* Clean up before returning. */
|
||||||
|
NSResetMapTable(_efdMap);
|
||||||
|
NSResetMapTable(_rfdMap);
|
||||||
|
NSResetMapTable(_wfdMap);
|
||||||
|
|
||||||
|
[self _checkPerformers];
|
||||||
|
GSNotifyASAP();
|
||||||
|
_current_mode = saved_mode;
|
||||||
}
|
}
|
||||||
while (fdIndex != fdEnd);
|
NS_HANDLER
|
||||||
|
{
|
||||||
|
_current_mode = saved_mode;
|
||||||
/* Clean up before returning. */
|
[localException raise];
|
||||||
NSResetMapTable(_efdMap);
|
}
|
||||||
NSResetMapTable(_rfdMap);
|
NS_ENDHANDLER
|
||||||
NSResetMapTable(_wfdMap);
|
|
||||||
|
|
||||||
[self _checkPerformers];
|
|
||||||
GSNotifyASAP();
|
|
||||||
_current_mode = saved_mode;
|
|
||||||
RELEASE(arp);
|
RELEASE(arp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,10 @@ static Class NSDate_class;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* This is the designated initializer. */
|
/*
|
||||||
|
* <init />
|
||||||
|
* Initialise a newly allocated NSTimer object.
|
||||||
|
*/
|
||||||
- (id) initWithTimeInterval: (NSTimeInterval)seconds
|
- (id) initWithTimeInterval: (NSTimeInterval)seconds
|
||||||
targetOrInvocation: (id)t
|
targetOrInvocation: (id)t
|
||||||
selector: (SEL)sel
|
selector: (SEL)sel
|
||||||
|
|
Loading…
Reference in a new issue