-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathSSYOperation.m
299 lines (260 loc) · 9.46 KB
/
SSYOperation.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
#import "SSYOperation.h"
#import "SSYOperationQueue.h"
#import "NSError+InfoAccess.h"
#import "NSDate+NiceFormats.h"
#import "NSError+MyDomain.h"
#if DEBUG
#if 0
#warning Logging running of operations in SSYOperation. DO NOT SHIP THIS!
/* For this to work, you must set the file path of the singleton
SSYLinearFileWriter object before beginning operations. */
#define LOG_RUNNING_OF_OPERATIONS 1
#import "SSYLinearFileWriter.h"
#endif
#endif
// We don't use 0 because that is the default condition of NSConditionLock if
// no condition has been set.
#define SSY_OPERATION_BLOCKED 1
#define SSY_OPERATION_CLEARED 2
@interface SSYOperation ()
@property (retain) id target ;
@property (assign) BOOL skipIfError ;
@property (retain) NSConditionLock* lock ;
/*
I added the 'lock' ivar in BookMacster 1.13.2, after receiving a crash
report from a user when invoking -[NSConditionLock unlockWithCondition:]
at the end of -unlockLock, seeing it crash once myself, and noticing
random behavior of lock's retainCount in -blockForLock. At this time, the
'lock' was not an ivar but was stored in SSYOperations' info dictionary.
Being in the dictionary was the only thing that retained it. It was placed
into this dictionary at the beginning of -prepareLock and removed at the end of
-blockForLock, by sending -removeObjectForKey:. Well it occurred to me that,
since -blockForLock and -unlockLock typically run on different threads, the
lock could be unlocked *enough* for -blockForLock to unblock, finish running,
and cause the lock to be deallocced before -unlockWithCondition was done
running, and if it sent a message to self during this time, indeed that would
cause a crash. So, first of all, I made it an instance variable instead, a
more conventional approach. I don't know why I did the dictioanary thing in
the first place. Probably it was thrown in ad hoc to solve some problem, and
I thought it would be rarely used and wanted to "save" an ivar. Stupid. But
the important part of the fix is that now I no longer remove it at the end
of -blockForLock. I let it persist until the next time that -prepareLock
is invoked, which sets a new instance into the ivar, releasing and
dealloccing the old one.
*/
@end
@implementation SSYOperation
@synthesize info = m_info ;
@synthesize target = m_target ;
@synthesize selector = m_selector ;
@synthesize operationQueue = m_operationQueue ;
@synthesize cancellor = m_cancellor ;
@synthesize skipIfError = m_skipIfError ;
@synthesize lock = m_lock ;
- (void)cancel {
[[self cancellor] invoke] ;
[self setInfo:nil];
[super cancel] ;
}
- (void)dealloc {
ssyDebugGlobalInteger--;
[m_info release] ;
[m_cancellor release] ;
[m_target release] ;
[m_lock release] ;
[super dealloc] ;
}
- (id)initWithInfo:(NSMutableDictionary*)info
target:(id)target
selector:(SEL)selector
operationQueue:(SSYOperationQueue*)operationQueue
skipIfError:(BOOL)skipIfError {
self = [super init] ;
if (self) {
ssyDebugGlobalInteger++;
[self setInfo:info] ;
[self setSelector:selector] ;
[self setTarget:target] ;
[self setOperationQueue:operationQueue] ;
[self setSkipIfError:skipIfError] ;
}
return self ;
}
- (NSError*)error {
NSError* error = [[self operationQueue] error] ;
if ([SSYOperationQueue operationGroupsDifferInfo:[error userInfo]
otherInfo:[self info]]) {
error = nil ;
}
return error ;
}
- (void)setError:(NSError*)error {
[[self operationQueue] setError:error
operation:self] ;
}
- (void)setAllGroupsError:(NSError*)error {
[[self operationQueue] setError:error
operation:nil] ;
}
- (NSString*)description {
NSString* selectorName = NSStringFromSelector([self selector]) ;
if ([selectorName isEqualToString:@"doDone:"]) {
selectorName = [selectorName stringByAppendingFormat:
@" (%@)",
[[self info] objectForKey:constKeySSYOperationQueueDoneSelectorName]] ;
}
return [NSString stringWithFormat:
@"%@ %p group=%@ selector=%@",
[self className],
self,
[[self info] objectForKey:constKeySSYOperationGroup],
selectorName] ;
}
- (void)doSafely:(SEL)cmdNameC {
NSString* selectorName = NSStringFromSelector(cmdNameC) ;
selectorName = [selectorName stringByAppendingString:@"_unsafe"] ;
[self performSelectorOnMainThread:NSSelectorFromString(selectorName)
withObject:nil
waitUntilDone:YES] ;
}
- (void)prepareLock {
// Normally, oldLock should be cleared at this point, if prior usage of
// this lock is done.
if ([self lockIsBlocking]) {
NSString* msg = nil ;
msg = [NSString stringWithFormat:@"Warning 209-8483 %@ lock=%@ info=%@",
self,
[self lock],
[self info] ];
NSLog(@"%@", msg) ;
#if DEBUG
NSAssert(NO, msg) ;
#endif
}
// I considered doing this:
// [oldLock unlockWithCondition:SSY_OPERATION_CLEARED] ;
// But am worried that it might raise an exception trying to unlock a lock on a
// thread that did not lock it. Maybe I'm just imagining that I ever saw that
// exception, or getting it mixed up with something else. Can't find it in
// documentation at the moment.
NSConditionLock* lock = [[NSConditionLock alloc] initWithCondition:SSY_OPERATION_BLOCKED] ;
// The above line released the old lock and replaced it with a new lock.
[self setLock:lock] ;
[lock release] ;
NSString* name = [NSString stringWithFormat:
@"SSYOperation's built-in lock created %@ by %@ on %@ (%@main)",
[[NSDate date] geekDateTimeString],
SSYDebugCaller(),
[NSThread currentThread],
([[NSThread currentThread] isMainThread] ? @"" : @"non-")] ;
[lock setName:name] ;
}
- (void)lockLock {
NSConditionLock* lock = [self lock] ;
// The following was added in BookMacster 1.11.2
BOOL succeeded = [lock tryLock] ;
if (!succeeded) {
// This will happen if the lock is already locked
NSLog(@"Warning 094-4701 %@", lock) ;
}
// Prior to BookMacster 1.11.2, the above was simply [lock lock]. I changed
// it to eliminate deadlock in case this method is called more than once
// prior to -unlockLock.
}
- (void)blockForLock {
NSConditionLock* lock = [self lock] ;
BOOL workFinishedInTime = [lock lockWhenCondition:SSY_OPERATION_CLEARED
beforeDate:[NSDate distantFuture]] ;
// Will block here until lock condition is SSY_OPERATION_CLEARED
if (workFinishedInTime) {
[lock unlock] ;
}
}
- (void)unlockLock {
// New code, BookMacster 1.11.2
// If we send -unlockWithCondition to a condition lock which has already
// been unlocked, Cocoa raises an exception. To avoid that, we try to
// lock it first. If it's already locked, tryLock is a no-op.
NSConditionLock* lock = [self lock] ;
// Local retain/release for additional safety. See
// http://lists.apple.com/archives/cocoa-dev/2013/Jan/msg00443.html
[lock retain] ;
[lock tryLock] ;
[lock unlockWithCondition:SSY_OPERATION_CLEARED] ;
[lock release] ;
}
- (BOOL)lockIsBlocking {
BOOL answer = NO ;
if ([self lock]) {
if ([[self lock] condition] != SSY_OPERATION_CLEARED) {
answer = YES ;
}
}
return answer ;
}
- (void)main {
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init] ;
#if LOG_RUNNING_OF_OPERATIONS
NSString* msg = [NSString stringWithFormat:
@"RunOp: %p grp=%@ prevErr=%ld nQ=%ld sel=%@ isCnc=%hhd",
self,
[[self info] objectForKey:constKeySSYOperationGroup],
(long)[[self error] code],
(long)[[[self operationQueue] operations] count], // nQ = number in queue
NSStringFromSelector([self selector]),
[self isCancelled]];
[SSYLinearFileWriter writeLine:msg];
#endif
if (![self isCancelled]) {
if (![[self operationQueue] shouldSkipOperationsInGroup:[[self info] objectForKey:constKeySSYOperationGroup]]) {
if (![self error] || ![self skipIfError]) {
@try {
id target = [self target] ;
if (target) {
[target performSelector:[self selector]
withObject:[self info]] ;
}
else {
// In this case, selector is usually defined in a category
target = self ;
[target performSelector:[self selector]] ;
}
}
@catch (NSException* exception) {
NSString* msg = @"An exception was raised." ;
NSError* error_ = SSYMakeError(56810, msg) ;
error_ = [error_ errorByAddingUnderlyingException:exception] ;
[self setError:error_] ;
}
@finally {
}
}
else {
// Chain operations have been aborted.
// This method becomes a no-op.
#if LOG_RUNNING_OF_OPERATIONS
NSString* msg = [[NSString alloc] initWithFormat:@"RunOp: Skipping %@ cuz error op=%p", NSStringFromSelector([self selector]), self];
[SSYLinearFileWriter writeLine:msg];
[msg release];
#endif
}
}
else {
#if LOG_RUNNING_OF_OPERATIONS
NSString* msg = [[NSString alloc] initWithFormat:@"RunOp: Skipping %@ cuz skip group %@ op=%p", NSStringFromSelector([self selector]), [[self info] objectForKey:constKeySSYOperationGroup], self];
[SSYLinearFileWriter writeLine:msg];
[msg release];
#endif
}
}
else {
#if LOG_RUNNING_OF_OPERATIONS
NSString* msg = [[NSString alloc] initWithFormat:@"RunOp: Skipping %@ cuz cancelled op=%p", NSStringFromSelector([self selector]), self];
[SSYLinearFileWriter writeLine:msg];
[msg release];
#endif
}
[self setInfo:nil];
[pool release] ;
}
@end