1/* 2 * 3 * Copyright 2019 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19#import "GRPCCallLegacy.h" 20 21#import "GRPCCall+OAuth2.h" 22#import "GRPCCallOptions.h" 23#import "GRPCTypes.h" 24 25#import "private/GRPCCore/GRPCChannelPool.h" 26#import "private/GRPCCore/GRPCCompletionQueue.h" 27#import "private/GRPCCore/GRPCHost.h" 28#import "private/GRPCCore/GRPCWrappedCall.h" 29#import "private/GRPCCore/NSData+GRPC.h" 30 31#import <RxLibrary/GRXBufferedPipe.h> 32#import <RxLibrary/GRXConcurrentWriteable.h> 33#import <RxLibrary/GRXImmediateSingleWriter.h> 34#import <RxLibrary/GRXWriter+Immediate.h> 35 36#include <grpc/grpc.h> 37 38const char *kCFStreamVarName = "grpc_cfstream"; 39static NSMutableDictionary *callFlags; 40 41// At most 6 ops can be in an op batch for a client: SEND_INITIAL_METADATA, 42// SEND_MESSAGE, SEND_CLOSE_FROM_CLIENT, RECV_INITIAL_METADATA, RECV_MESSAGE, 43// and RECV_STATUS_ON_CLIENT. 44NSInteger kMaxClientBatch = 6; 45 46static NSString *const kAuthorizationHeader = @"authorization"; 47static NSString *const kBearerPrefix = @"Bearer "; 48 49@interface GRPCCall () <GRXWriteable> 50// Make them read-write. 51@property(atomic, copy) NSDictionary *responseHeaders; 52@property(atomic, copy) NSDictionary *responseTrailers; 53 54- (void)receiveNextMessages:(NSUInteger)numberOfMessages; 55 56@end 57 58// The following methods of a C gRPC call object aren't reentrant, and thus 59// calls to them must be serialized: 60// - start_batch 61// - destroy 62// 63// start_batch with a SEND_MESSAGE argument can only be called after the 64// OP_COMPLETE event for any previous write is received. This is achieved by 65// pausing the requests writer immediately every time it writes a value, and 66// resuming it again when OP_COMPLETE is received. 67// 68// Similarly, start_batch with a RECV_MESSAGE argument can only be called after 69// the OP_COMPLETE event for any previous read is received.This is easier to 70// enforce, as we're writing the received messages into the writeable: 71// start_batch is enqueued once upon receiving the OP_COMPLETE event for the 72// RECV_METADATA batch, and then once after receiving each OP_COMPLETE event for 73// each RECV_MESSAGE batch. 74@implementation GRPCCall { 75 dispatch_queue_t _callQueue; 76 77 NSString *_host; 78 NSString *_path; 79 GRPCCallSafety _callSafety; 80 GRPCCallOptions *_callOptions; 81 GRPCWrappedCall *_wrappedCall; 82 83 // The C gRPC library has less guarantees on the ordering of events than we 84 // do. Particularly, in the face of errors, there's no ordering guarantee at 85 // all. This wrapper over our actual writeable ensures thread-safety and 86 // correct ordering. 87 GRXConcurrentWriteable *_responseWriteable; 88 89 // The network thread wants the requestWriter to resume (when the server is ready for more input), 90 // or to stop (on errors), concurrently with user threads that want to start it, pause it or stop 91 // it. Because a writer isn't thread-safe, we'll synchronize those operations on it. 92 // We don't use a dispatch queue for that purpose, because the writer can call writeValue: or 93 // writesFinishedWithError: on this GRPCCall as part of those operations. We want to be able to 94 // pause the writer immediately on writeValue:, so we need our locking to be recursive. 95 GRXWriter *_requestWriter; 96 97 // To create a retain cycle when a call is started, up until it finishes. See 98 // |startWithWriteable:| and |finishWithError:|. This saves users from having to retain a 99 // reference to the call object if all they're interested in is the handler being executed when 100 // the response arrives. 101 GRPCCall *_retainSelf; 102 103 GRPCRequestHeaders *_requestHeaders; 104 105 // In the case that the call is a unary call (i.e. the writer to GRPCCall is of type 106 // GRXImmediateSingleWriter), GRPCCall will delay sending ops (not send them to C core 107 // immediately) and buffer them into a batch _unaryOpBatch. The batch is sent to C core when 108 // the SendClose op is added. 109 BOOL _unaryCall; 110 NSMutableArray *_unaryOpBatch; 111 112 // The dispatch queue to be used for enqueuing responses to user. Defaulted to the main dispatch 113 // queue 114 dispatch_queue_t _responseQueue; 115 116 // The OAuth2 token fetched from a token provider. 117 NSString *_fetchedOauth2AccessToken; 118 119 // The callback to be called when a write message op is done. 120 void (^_writeDone)(void); 121 122 // Indicate a read request to core is pending. 123 BOOL _pendingCoreRead; 124 125 // Indicate pending read message request from user. 126 NSUInteger _pendingReceiveNextMessages; 127} 128 129@synthesize state = _state; 130 131+ (void)initialize { 132 // Guarantees the code in {} block is invoked only once. See ref at: 133 // https://developer.apple.com/documentation/objectivec/nsobject/1418639-initialize?language=objc 134 if (self == [GRPCCall self]) { 135 grpc_init(); 136 callFlags = [NSMutableDictionary dictionary]; 137 } 138} 139 140+ (void)setCallSafety:(GRPCCallSafety)callSafety host:(NSString *)host path:(NSString *)path { 141 if (host.length == 0 || path.length == 0) { 142 return; 143 } 144 NSString *hostAndPath = [NSString stringWithFormat:@"%@/%@", host, path]; 145 @synchronized(callFlags) { 146 switch (callSafety) { 147 case GRPCCallSafetyDefault: 148 callFlags[hostAndPath] = @0; 149 break; 150 default: 151 break; 152 } 153 } 154} 155 156+ (uint32_t)callFlagsForHost:(NSString *)host path:(NSString *)path { 157 NSString *hostAndPath = [NSString stringWithFormat:@"%@/%@", host, path]; 158 @synchronized(callFlags) { 159 return [callFlags[hostAndPath] intValue]; 160 } 161} 162 163- (instancetype)initWithHost:(NSString *)host 164 path:(NSString *)path 165 requestsWriter:(GRXWriter *)requestWriter { 166 return [self initWithHost:host 167 path:path 168 callSafety:GRPCCallSafetyDefault 169 requestsWriter:requestWriter 170 callOptions:nil 171 writeDone:nil]; 172} 173 174- (instancetype)initWithHost:(NSString *)host 175 path:(NSString *)path 176 callSafety:(GRPCCallSafety)safety 177 requestsWriter:(GRXWriter *)requestsWriter 178 callOptions:(GRPCCallOptions *)callOptions 179 writeDone:(void (^)(void))writeDone { 180 // Purposely using pointer rather than length (host.length == 0) for backwards compatibility. 181 NSAssert(host != nil && path != nil, @"Neither host nor path can be nil."); 182 NSAssert(safety <= GRPCCallSafetyDefault, @"Invalid call safety value."); 183 NSAssert(requestsWriter.state == GRXWriterStateNotStarted, 184 @"The requests writer can't be already started."); 185 if (!host || !path) { 186 return nil; 187 } 188 if (requestsWriter.state != GRXWriterStateNotStarted) { 189 return nil; 190 } 191 192 if ((self = [super init])) { 193 _host = [host copy]; 194 _path = [path copy]; 195 _callSafety = safety; 196 _callOptions = [callOptions copy]; 197 198 // Serial queue to invoke the non-reentrant methods of the grpc_call object. 199 _callQueue = dispatch_queue_create("io.grpc.call", DISPATCH_QUEUE_SERIAL); 200 201 _requestWriter = requestsWriter; 202 _requestHeaders = [[GRPCRequestHeaders alloc] initWithCall:self]; 203 _writeDone = writeDone; 204 205 if ([requestsWriter isKindOfClass:[GRXImmediateSingleWriter class]]) { 206 _unaryCall = YES; 207 _unaryOpBatch = [NSMutableArray arrayWithCapacity:kMaxClientBatch]; 208 } 209 210 _responseQueue = dispatch_get_main_queue(); 211 212 // do not start a read until initial metadata is received 213 _pendingReceiveNextMessages = 0; 214 _pendingCoreRead = YES; 215 } 216 return self; 217} 218 219- (void)setResponseDispatchQueue:(dispatch_queue_t)queue { 220 @synchronized(self) { 221 if (_state != GRXWriterStateNotStarted) { 222 return; 223 } 224 _responseQueue = queue; 225 } 226} 227 228#pragma mark Finish 229 230// This function should support being called within a @synchronized(self) block in another function 231// Should not manipulate _requestWriter for deadlock prevention. 232- (void)finishWithError:(NSError *)errorOrNil { 233 @synchronized(self) { 234 if (_state == GRXWriterStateFinished) { 235 return; 236 } 237 _state = GRXWriterStateFinished; 238 239 if (errorOrNil) { 240 [_responseWriteable cancelWithError:errorOrNil]; 241 } else { 242 [_responseWriteable enqueueSuccessfulCompletion]; 243 } 244 245 // If the call isn't retained anywhere else, it can be deallocated now. 246 _retainSelf = nil; 247 } 248} 249 250- (void)cancel { 251 @synchronized(self) { 252 if (_state == GRXWriterStateFinished) { 253 return; 254 } 255 [self finishWithError:[NSError 256 errorWithDomain:kGRPCErrorDomain 257 code:GRPCErrorCodeCancelled 258 userInfo:@{NSLocalizedDescriptionKey : @"Canceled by app"}]]; 259 [_wrappedCall cancel]; 260 } 261 _requestWriter.state = GRXWriterStateFinished; 262} 263 264- (void)dealloc { 265 if (_callQueue) { 266 __block GRPCWrappedCall *wrappedCall = _wrappedCall; 267 dispatch_async(_callQueue, ^{ 268 wrappedCall = nil; 269 }); 270 } else { 271 _wrappedCall = nil; 272 } 273} 274 275#pragma mark Read messages 276 277// Only called from the call queue. 278// The handler will be called from the network queue. 279- (void)startReadWithHandler:(void (^)(grpc_byte_buffer *))handler { 280 // TODO(jcanizales): Add error handlers for async failures 281 [_wrappedCall startBatchWithOperations:@[ [[GRPCOpRecvMessage alloc] initWithHandler:handler] ]]; 282} 283 284// Called initially from the network queue once response headers are received, 285// then "recursively" from the responseWriteable queue after each response from the 286// server has been written. 287// If the call is currently paused, this is a noop. Restarting the call will invoke this 288// method. 289// TODO(jcanizales): Rename to readResponseIfNotPaused. 290- (void)maybeStartNextRead { 291 @synchronized(self) { 292 if (_state != GRXWriterStateStarted) { 293 return; 294 } 295 if (_callOptions.flowControlEnabled && (_pendingCoreRead || _pendingReceiveNextMessages == 0)) { 296 return; 297 } 298 _pendingCoreRead = YES; 299 _pendingReceiveNextMessages--; 300 } 301 302 dispatch_async(_callQueue, ^{ 303 __weak GRPCCall *weakSelf = self; 304 [self startReadWithHandler:^(grpc_byte_buffer *message) { 305 if (message == NULL) { 306 // No more messages from the server 307 return; 308 } 309 __strong GRPCCall *strongSelf = weakSelf; 310 if (strongSelf == nil) { 311 grpc_byte_buffer_destroy(message); 312 return; 313 } 314 NSData *data = [NSData grpc_dataWithByteBuffer:message]; 315 grpc_byte_buffer_destroy(message); 316 if (!data) { 317 // The app doesn't have enough memory to hold the server response. We 318 // don't want to throw, because the app shouldn't crash for a behavior 319 // that's on the hands of any server to have. Instead we finish and ask 320 // the server to cancel. 321 @synchronized(strongSelf) { 322 strongSelf->_pendingCoreRead = NO; 323 [strongSelf 324 finishWithError:[NSError errorWithDomain:kGRPCErrorDomain 325 code:GRPCErrorCodeResourceExhausted 326 userInfo:@{ 327 NSLocalizedDescriptionKey : 328 @"Client does not have enough memory to " 329 @"hold the server response." 330 }]]; 331 [strongSelf->_wrappedCall cancel]; 332 } 333 strongSelf->_requestWriter.state = GRXWriterStateFinished; 334 } else { 335 @synchronized(strongSelf) { 336 [strongSelf->_responseWriteable enqueueValue:data 337 completionHandler:^{ 338 __strong GRPCCall *strongSelf = weakSelf; 339 if (strongSelf) { 340 @synchronized(strongSelf) { 341 strongSelf->_pendingCoreRead = NO; 342 [strongSelf maybeStartNextRead]; 343 } 344 } 345 }]; 346 } 347 } 348 }]; 349 }); 350} 351 352#pragma mark Send headers 353 354- (void)sendHeaders { 355 // TODO (mxyan): Remove after deprecated methods are removed 356 uint32_t callSafetyFlags = 0; 357 358 NSMutableDictionary *headers = [_requestHeaders mutableCopy]; 359 NSString *fetchedOauth2AccessToken; 360 @synchronized(self) { 361 fetchedOauth2AccessToken = _fetchedOauth2AccessToken; 362 } 363 if (fetchedOauth2AccessToken != nil) { 364 headers[@"authorization"] = [kBearerPrefix stringByAppendingString:fetchedOauth2AccessToken]; 365 } else if (_callOptions.oauth2AccessToken != nil) { 366 headers[@"authorization"] = 367 [kBearerPrefix stringByAppendingString:_callOptions.oauth2AccessToken]; 368 } 369 370 // TODO(jcanizales): Add error handlers for async failures 371 GRPCOpSendMetadata *op = [[GRPCOpSendMetadata alloc] 372 initWithMetadata:headers 373 flags:callSafetyFlags 374 handler:nil]; // No clean-up needed after SEND_INITIAL_METADATA 375 dispatch_async(_callQueue, ^{ 376 if (!self->_unaryCall) { 377 [self->_wrappedCall startBatchWithOperations:@[ op ]]; 378 } else { 379 [self->_unaryOpBatch addObject:op]; 380 } 381 }); 382} 383 384- (void)receiveNextMessages:(NSUInteger)numberOfMessages { 385 if (numberOfMessages == 0) { 386 return; 387 } 388 @synchronized(self) { 389 _pendingReceiveNextMessages += numberOfMessages; 390 391 if (_state != GRXWriterStateStarted || !_callOptions.flowControlEnabled) { 392 return; 393 } 394 [self maybeStartNextRead]; 395 } 396} 397 398#pragma mark GRXWriteable implementation 399 400// Only called from the call queue. The error handler will be called from the 401// network queue if the write didn't succeed. 402// If the call is a unary call, parameter \a errorHandler will be ignored and 403// the error handler of GRPCOpSendClose will be executed in case of error. 404- (void)writeMessage:(NSData *)message withErrorHandler:(void (^)(void))errorHandler { 405 __weak GRPCCall *weakSelf = self; 406 void (^resumingHandler)(void) = ^{ 407 // Resume the request writer. 408 GRPCCall *strongSelf = weakSelf; 409 if (strongSelf) { 410 strongSelf->_requestWriter.state = GRXWriterStateStarted; 411 if (strongSelf->_writeDone) { 412 strongSelf->_writeDone(); 413 } 414 } 415 }; 416 GRPCOpSendMessage *op = [[GRPCOpSendMessage alloc] initWithMessage:message 417 handler:resumingHandler]; 418 if (!_unaryCall) { 419 [_wrappedCall startBatchWithOperations:@[ op ] errorHandler:errorHandler]; 420 } else { 421 // Ignored errorHandler since it is the same as the one for GRPCOpSendClose. 422 // TODO (mxyan): unify the error handlers of all Ops into a single closure. 423 [_unaryOpBatch addObject:op]; 424 } 425} 426 427- (void)writeValue:(id)value { 428 NSAssert([value isKindOfClass:[NSData class]], @"value must be of type NSData"); 429 430 @synchronized(self) { 431 if (_state == GRXWriterStateFinished) { 432 return; 433 } 434 } 435 436 // Pause the input and only resume it when the C layer notifies us that writes 437 // can proceed. 438 _requestWriter.state = GRXWriterStatePaused; 439 440 dispatch_async(_callQueue, ^{ 441 // Write error is not processed here. It is handled by op batch of GRPC_OP_RECV_STATUS_ON_CLIENT 442 [self writeMessage:value withErrorHandler:nil]; 443 }); 444} 445 446// Only called from the call queue. The error handler will be called from the 447// network queue if the requests stream couldn't be closed successfully. 448- (void)finishRequestWithErrorHandler:(void (^)(void))errorHandler { 449 if (!_unaryCall) { 450 [_wrappedCall startBatchWithOperations:@[ [[GRPCOpSendClose alloc] init] ] 451 errorHandler:errorHandler]; 452 } else { 453 [_unaryOpBatch addObject:[[GRPCOpSendClose alloc] init]]; 454 [_wrappedCall startBatchWithOperations:_unaryOpBatch errorHandler:errorHandler]; 455 } 456} 457 458- (void)writesFinishedWithError:(NSError *)errorOrNil { 459 if (errorOrNil) { 460 [self cancel]; 461 } else { 462 dispatch_async(_callQueue, ^{ 463 // EOS error is not processed here. It is handled by op batch of GRPC_OP_RECV_STATUS_ON_CLIENT 464 [self finishRequestWithErrorHandler:nil]; 465 }); 466 } 467} 468 469#pragma mark Invoke 470 471// Both handlers will eventually be called, from the network queue. Writes can start immediately 472// after this. 473// The first one (headersHandler), when the response headers are received. 474// The second one (completionHandler), whenever the RPC finishes for any reason. 475- (void)invokeCallWithHeadersHandler:(void (^)(NSDictionary *))headersHandler 476 completionHandler:(void (^)(NSError *, NSDictionary *))completionHandler { 477 dispatch_async(_callQueue, ^{ 478 // TODO(jcanizales): Add error handlers for async failures 479 [self->_wrappedCall 480 startBatchWithOperations:@[ [[GRPCOpRecvMetadata alloc] initWithHandler:headersHandler] ]]; 481 [self->_wrappedCall 482 startBatchWithOperations:@[ [[GRPCOpRecvStatus alloc] initWithHandler:completionHandler] ]]; 483 }); 484} 485 486- (void)invokeCall { 487 __weak GRPCCall *weakSelf = self; 488 [self 489 invokeCallWithHeadersHandler:^(NSDictionary *headers) { 490 // Response headers received. 491 __strong GRPCCall *strongSelf = weakSelf; 492 if (strongSelf) { 493 @synchronized(strongSelf) { 494 // it is ok to set nil because headers are only received once 495 strongSelf.responseHeaders = nil; 496 // copy the header so that the GRPCOpRecvMetadata object may be dealloc'ed 497 NSDictionary *copiedHeaders = [[NSDictionary alloc] initWithDictionary:headers 498 copyItems:YES]; 499 strongSelf.responseHeaders = copiedHeaders; 500 strongSelf->_pendingCoreRead = NO; 501 [strongSelf maybeStartNextRead]; 502 } 503 } 504 } 505 completionHandler:^(NSError *error, NSDictionary *trailers) { 506 __strong GRPCCall *strongSelf = weakSelf; 507 if (strongSelf) { 508 strongSelf.responseTrailers = trailers; 509 510 if (error) { 511 NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; 512 if (error.userInfo) { 513 [userInfo addEntriesFromDictionary:error.userInfo]; 514 } 515 userInfo[kGRPCTrailersKey] = strongSelf.responseTrailers; 516 // Since gRPC core does not guarantee the headers block being called before this block, 517 // responseHeaders might be nil. 518 userInfo[kGRPCHeadersKey] = strongSelf.responseHeaders; 519 error = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo]; 520 } 521 [strongSelf finishWithError:error]; 522 strongSelf->_requestWriter.state = GRXWriterStateFinished; 523 } 524 }]; 525} 526 527#pragma mark GRXWriter implementation 528 529// Lock acquired inside startWithWriteable: 530- (void)startCallWithWriteable:(id<GRXWriteable>)writeable { 531 @synchronized(self) { 532 if (_state == GRXWriterStateFinished) { 533 return; 534 } 535 536 _responseWriteable = [[GRXConcurrentWriteable alloc] initWithWriteable:writeable 537 dispatchQueue:_responseQueue]; 538 539 GRPCPooledChannel *channel = [[GRPCChannelPool sharedInstance] channelWithHost:_host 540 callOptions:_callOptions]; 541 _wrappedCall = [channel wrappedCallWithPath:_path 542 completionQueue:[GRPCCompletionQueue completionQueue] 543 callOptions:_callOptions]; 544 545 if (_wrappedCall == nil) { 546 [self finishWithError:[NSError errorWithDomain:kGRPCErrorDomain 547 code:GRPCErrorCodeUnavailable 548 userInfo:@{ 549 NSLocalizedDescriptionKey : 550 @"Failed to create call or channel." 551 }]]; 552 return; 553 } 554 555 [self sendHeaders]; 556 [self invokeCall]; 557 } 558 559 // Now that the RPC has been initiated, request writes can start. 560 [_requestWriter startWithWriteable:self]; 561} 562 563- (void)startWithWriteable:(id<GRXWriteable>)writeable { 564 id<GRPCAuthorizationProtocol> tokenProvider = nil; 565 @synchronized(self) { 566 _state = GRXWriterStateStarted; 567 568 // Create a retain cycle so that this instance lives until the RPC finishes (or is cancelled). 569 // This makes RPCs in which the call isn't externally retained possible (as long as it is 570 // started before being autoreleased). Care is taken not to retain self strongly in any of the 571 // blocks used in this implementation, so that the life of the instance is determined by this 572 // retain cycle. 573 _retainSelf = self; 574 575 // If _callOptions is nil, people must be using the deprecated v1 interface. In this case, 576 // generate the call options from the corresponding GRPCHost configs and apply other options 577 // that are not covered by GRPCHost. 578 if (_callOptions == nil) { 579 GRPCMutableCallOptions *callOptions = [[GRPCHost callOptionsForHost:_host] mutableCopy]; 580 if (_serverName.length != 0) { 581 callOptions.serverAuthority = _serverName; 582 } 583 if (_timeout > 0) { 584 callOptions.timeout = _timeout; 585 } 586 587 id<GRPCAuthorizationProtocol> tokenProvider = self.tokenProvider; 588 if (tokenProvider != nil) { 589 callOptions.authTokenProvider = tokenProvider; 590 } 591 _callOptions = callOptions; 592 } 593 594 NSAssert(_callOptions.authTokenProvider == nil || _callOptions.oauth2AccessToken == nil, 595 @"authTokenProvider and oauth2AccessToken cannot be set at the same time"); 596 597 tokenProvider = _callOptions.authTokenProvider; 598 } 599 600 if (tokenProvider != nil) { 601 __weak auto weakSelf = self; 602 [tokenProvider getTokenWithHandler:^(NSString *token) { 603 __strong auto strongSelf = weakSelf; 604 if (strongSelf) { 605 BOOL startCall = NO; 606 @synchronized(strongSelf) { 607 if (strongSelf->_state != GRXWriterStateFinished) { 608 startCall = YES; 609 if (token) { 610 strongSelf->_fetchedOauth2AccessToken = [token copy]; 611 } 612 } 613 } 614 if (startCall) { 615 [strongSelf startCallWithWriteable:writeable]; 616 } 617 } 618 }]; 619 } else { 620 [self startCallWithWriteable:writeable]; 621 } 622} 623 624- (void)setState:(GRXWriterState)newState { 625 @synchronized(self) { 626 // Manual transitions are only allowed from the started or paused states. 627 if (_state == GRXWriterStateNotStarted || _state == GRXWriterStateFinished) { 628 return; 629 } 630 631 switch (newState) { 632 case GRXWriterStateFinished: 633 _state = newState; 634 // Per GRXWriter's contract, setting the state to Finished manually 635 // means one doesn't wish the writeable to be messaged anymore. 636 [_responseWriteable cancelSilently]; 637 _responseWriteable = nil; 638 return; 639 case GRXWriterStatePaused: 640 _state = newState; 641 return; 642 case GRXWriterStateStarted: 643 if (_state == GRXWriterStatePaused) { 644 _state = newState; 645 [self maybeStartNextRead]; 646 } 647 return; 648 case GRXWriterStateNotStarted: 649 return; 650 } 651 } 652} 653 654@end 655