| // |
| // Copyright 2019 Google LLC. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| // |
| |
| #import "Service/Sources/EDOClientService.h" |
| |
| #include <objc/runtime.h> |
| |
| #import "Channel/Sources/EDOChannel.h" |
| #import "Channel/Sources/EDOChannelPool.h" |
| #import "Channel/Sources/EDOHostPort.h" |
| #import "Service/Sources/EDOBlockObject.h" |
| #import "Service/Sources/EDOClassMessage.h" |
| #import "Service/Sources/EDOClientService+Private.h" |
| #import "Service/Sources/EDOClientServiceStatsCollector.h" |
| #import "Service/Sources/EDOExecutor.h" |
| #import "Service/Sources/EDOHostNamingService.h" |
| #import "Service/Sources/EDOHostService+Private.h" |
| #import "Service/Sources/EDOHostService.h" |
| #import "Service/Sources/EDOMessage.h" |
| #import "Service/Sources/EDOObject+Private.h" |
| #import "Service/Sources/EDOObject.h" |
| #import "Service/Sources/EDOObjectAliveMessage.h" |
| #import "Service/Sources/EDOObjectMessage.h" |
| #import "Service/Sources/EDOObjectReleaseMessage.h" |
| #import "Service/Sources/EDOServiceError.h" |
| #import "Service/Sources/EDOServiceException.h" |
| #import "Service/Sources/EDOServicePort.h" |
| #import "Service/Sources/EDOServiceRequest.h" |
| #import "Service/Sources/EDOTimingFunctions.h" |
| #import "Service/Sources/NSKeyedArchiver+EDOAdditions.h" |
| #import "Service/Sources/NSKeyedUnarchiver+EDOAdditions.h" |
| |
| /** Timeout for ping health check. */ |
| static const int64_t kPingTimeoutSeconds = 10 * NSEC_PER_SEC; |
| |
| /** The default error handler that throws an exception with an embedded error object. */ |
| static const EDOClientErrorHandler kEDOClientDefaultErrorHandler = ^(NSError *error) { |
| [[NSException exceptionWithName:EDOServiceGenericException |
| reason:error.description |
| userInfo:@{EDOExceptionUnderlyingErrorKey : error}] raise]; |
| }; |
| |
| /** The global error handler for the client. */ |
| static EDOClientErrorHandler gEDOClientErrorHandler = kEDOClientDefaultErrorHandler; |
| |
| EDOClientErrorHandler EDOSetClientErrorHandler(EDOClientErrorHandler errorHandler) { |
| // Move @c errorHandler to heap as the handler will be used globally. |
| errorHandler = |
| errorHandler ? (EDOClientErrorHandler)[errorHandler copy] : kEDOClientDefaultErrorHandler; |
| @synchronized([EDOClientService class]) { |
| EDOClientErrorHandler oldErrorHandler = gEDOClientErrorHandler; |
| gEDOClientErrorHandler = errorHandler; |
| return oldErrorHandler; |
| } |
| } |
| |
| @implementation EDOClientService |
| |
| + (id)rootObjectWithHostPort:(EDOHostPort *)hostPort { |
| EDOObjectRequest *objectRequest = [EDOObjectRequest requestWithHostPort:hostPort]; |
| return [self responseObjectWithRequest:objectRequest onPort:hostPort]; |
| } |
| |
| + (id)rootObjectWithPort:(UInt16)port { |
| return [self rootObjectWithHostPort:[EDOHostPort hostPortWithLocalPort:port]]; |
| } |
| |
| + (id)rootObjectWithServiceName:(NSString *)serviceName { |
| EDOHostPort *hostPort = [EDOHostPort hostPortWithLocalPort:0 serviceName:serviceName]; |
| return [self rootObjectWithHostPort:hostPort]; |
| } |
| |
| + (Class)classObjectWithName:(NSString *)className hostPort:(EDOHostPort *)hostPort { |
| EDOServiceRequest *classRequest = [EDOClassRequest requestWithClassName:className |
| hostPort:hostPort]; |
| return (Class)[self responseObjectWithRequest:classRequest onPort:hostPort]; |
| } |
| |
| + (Class)classObjectWithName:(NSString *)className port:(UInt16)port { |
| EDOHostPort *hostPort = [EDOHostPort hostPortWithLocalPort:port]; |
| return [self classObjectWithName:className hostPort:hostPort]; |
| } |
| |
| + (Class)classObjectWithName:(NSString *)className serviceName:(NSString *)serviceName { |
| EDOHostPort *hostPort = [EDOHostPort hostPortWithLocalPort:0 serviceName:serviceName]; |
| return [self classObjectWithName:className hostPort:hostPort]; |
| } |
| |
| + (id)unwrappedObjectFromObject:(id)object { |
| EDOObject *edoObject = |
| [EDOBlockObject isBlock:object] ? [EDOBlockObject EDOBlockObjectFromBlock:object] : object; |
| Class objClass = object_getClass(edoObject); |
| if (objClass == [EDOObject class] || objClass == [EDOBlockObject class]) { |
| EDOHostService *service = [EDOHostService serviceForCurrentOriginatingQueue]; |
| if (!service) { |
| service = [EDOHostService temporaryServiceForCurrentThread]; |
| } |
| // If there is a service for the current queue, we check if the object belongs to this queue. |
| // Otherwise, we send EDOObjectAlive message to another service running in the same process. |
| if ([service isObjectAliveWithPort:edoObject.servicePort |
| remoteAddress:edoObject.remoteAddress]) { |
| return (__bridge id)(void *)edoObject.remoteAddress; |
| } else if (edoObject.isLocalEdo) { |
| // If the underlying object is not a local object (but in the same process) then this could |
| // return nil. For example, the service becomes invalide, or the remote object is already |
| // released. |
| id resolvedInstance = [self resolveInstanceFromEDOObject:edoObject]; |
| if (resolvedInstance) { |
| return resolvedInstance; |
| } |
| } |
| } |
| |
| return object; |
| } |
| |
| + (EDOHostNamingService *)namingServiceWithDeviceSerial:(NSString *)serial error:(NSError **)error { |
| @try { |
| EDOHostPort *hostPort = [EDOHostPort hostPortWithPort:EDOHostNamingService.namingServerPort |
| name:nil |
| deviceSerialNumber:serial]; |
| EDOObjectRequest *objectRequest = [EDOObjectRequest requestWithHostPort:hostPort]; |
| return (EDOHostNamingService *)[self responseObjectWithRequest:objectRequest onPort:hostPort]; |
| } @catch (NSException *exception) { |
| if (error) { |
| *error = [NSError errorWithDomain:EDOServiceErrorDomain |
| code:EDOServiceErrorNamingServiceUnavailable |
| userInfo:@{NSLocalizedDescriptionKey : exception.reason}]; |
| } else { |
| NSLog(@"Failed to fetch naming service remote object: %@.", exception); |
| } |
| } |
| return nil; |
| } |
| |
| + (BOOL)isServiceAvailableOnPort:(UInt16)port { |
| EDOHostPort *hostPort = [EDOHostPort hostPortWithLocalPort:port]; |
| EDOServiceRequest *classRequest = |
| [EDOClassRequest requestWithClassName:NSStringFromClass([NSObject class]) hostPort:hostPort]; |
| EDOHostService *service = [EDOHostService serviceForCurrentExecutingQueue]; |
| NSError *error = nil; |
| [self sendSynchronousRequest:classRequest |
| onPort:hostPort |
| withExecutor:service.executor |
| error:&error]; |
| return error == nil; |
| } |
| |
| #pragma mark - Private Category |
| |
| + (NSMapTable *)localDistantObjects { |
| static NSMapTable<NSNumber *, EDOObject *> *localDistantObjects; |
| static dispatch_once_t onceToken; |
| dispatch_once(&onceToken, ^{ |
| localDistantObjects = [NSMapTable strongToWeakObjectsMapTable]; |
| }); |
| return localDistantObjects; |
| } |
| |
| + (dispatch_queue_t)edoSyncQueue { |
| static dispatch_queue_t edoSyncQueue; |
| static dispatch_once_t onceToken; |
| dispatch_once(&onceToken, ^{ |
| edoSyncQueue = dispatch_queue_create("com.google.edo.service.edoSync", DISPATCH_QUEUE_SERIAL); |
| }); |
| return edoSyncQueue; |
| } |
| |
| + (NSData *)pingMessageData { |
| static NSData *_pingMessageData; |
| static dispatch_once_t onceToken; |
| dispatch_once(&onceToken, ^{ |
| _pingMessageData = [@"ping" dataUsingEncoding:NSUTF8StringEncoding]; |
| }); |
| return _pingMessageData; |
| } |
| |
| + (EDOObject *)distantObjectReferenceForRemoteAddress:(EDOPointerType)remoteAddress { |
| NSNumber *edoKey = [NSNumber numberWithLongLong:remoteAddress]; |
| __block EDOObject *result; |
| dispatch_sync(self.edoSyncQueue, ^{ |
| result = [self.localDistantObjects objectForKey:edoKey]; |
| }); |
| return result; |
| } |
| |
| + (void)addDistantObjectReference:(id)object { |
| EDOObject *edoObject = |
| [EDOBlockObject isBlock:object] ? [EDOBlockObject EDOBlockObjectFromBlock:object] : object; |
| NSNumber *edoKey = [NSNumber numberWithLongLong:edoObject.remoteAddress]; |
| dispatch_sync(self.edoSyncQueue, ^{ |
| [self.localDistantObjects setObject:object forKey:edoKey]; |
| }); |
| } |
| |
| + (void)removeDistantObjectReference:(EDOPointerType)remoteAddress { |
| NSNumber *edoKey = [NSNumber numberWithLongLong:remoteAddress]; |
| dispatch_sync(self.edoSyncQueue, ^{ |
| [self.localDistantObjects removeObjectForKey:edoKey]; |
| }); |
| } |
| |
| + (id)cachedEDOFromObjectUpdateIfNeeded:(id)object { |
| EDOObject *edoObject = |
| [EDOBlockObject isBlock:object] ? [EDOBlockObject EDOBlockObjectFromBlock:object] : object; |
| Class objClass = object_getClass(edoObject); |
| if (objClass == [EDOObject class] || objClass == [EDOBlockObject class]) { |
| id localObject = [self distantObjectReferenceForRemoteAddress:edoObject.remoteAddress]; |
| EDOObject *localEDO = localObject; |
| if ([EDOBlockObject isBlock:localObject]) { |
| localEDO = [EDOBlockObject EDOBlockObjectFromBlock:localEDO]; |
| } |
| // Verify the service in case the old address is overwritten by a new service. |
| if ([edoObject.servicePort match:localEDO.servicePort]) { |
| // Since we already have the EDOObject in the cache, the new decoded EDOObject is |
| // taken as a temporary local object, which does not send release message. |
| edoObject.local = YES; |
| return localObject; |
| } else { |
| // Track the new remote object. |
| [self addDistantObjectReference:object]; |
| } |
| } |
| return object; |
| } |
| |
| + (EDOServiceResponse *)sendSynchronousRequest:(EDOServiceRequest *)request |
| onPort:(EDOHostPort *)port { |
| // TODO(b/119416282): We still run the executor even for the other requests before the deadlock |
| // issue is fixed. |
| EDOHostService *service = [EDOHostService serviceForCurrentExecutingQueue]; |
| return [self sendSynchronousRequest:request onPort:port withExecutor:service.executor]; |
| } |
| |
| + (EDOServiceResponse *)sendSynchronousRequest:(EDOServiceRequest *)request |
| onPort:(EDOHostPort *)port |
| withExecutor:(EDOExecutor *)executor { |
| return [self sendSynchronousRequest:request onPort:port withExecutor:executor error:NULL]; |
| } |
| |
| + (EDOServiceResponse *)sendSynchronousRequest:(EDOServiceRequest *)request |
| onPort:(EDOHostPort *)port |
| withExecutor:(EDOExecutor *)executor |
| error:(NSError **)errorOut { |
| EDOClientServiceStatsCollector *stats = EDOClientServiceStatsCollector.sharedServiceStats; |
| |
| int maxAttempts = 2; |
| int currentAttempt = 0; |
| while (currentAttempt < maxAttempts) { |
| NSError *connectionError; |
| uint64_t connectionStartTime = mach_absolute_time(); |
| dispatch_queue_t executionQueue = executor.executionQueue; |
| dispatch_qos_class_t qosClass = |
| executionQueue ? dispatch_queue_get_qos_class(executionQueue, nil) : qos_class_self(); |
| dispatch_queue_attr_t queueAttributes = |
| dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, qosClass, 0); |
| dispatch_queue_t connectionQueue = |
| dispatch_queue_create("com.google.edo.connectChannel", queueAttributes); |
| id<EDOChannel> channel = [EDOChannelPool.sharedChannelPool channelWithPort:port |
| connectionQueue:connectionQueue |
| error:&connectionError]; |
| [stats reportConnectionDuration:EDOGetMillisecondsSinceMachTime(connectionStartTime)]; |
| |
| if (connectionError) { |
| [stats reportError]; |
| NSDictionary<NSErrorUserInfoKey, id> *userInfo = @{ |
| EDOErrorPortKey : port, |
| EDOErrorRequestKey : request.description, |
| EDOErrorConnectAttemptKey : @(currentAttempt), |
| NSUnderlyingErrorKey : connectionError |
| }; |
| NSError *error = [NSError errorWithDomain:EDOServiceErrorDomain |
| code:EDOServiceErrorCannotConnect |
| userInfo:userInfo]; |
| if (errorOut != NULL) { |
| *errorOut = error; |
| } else { |
| gEDOClientErrorHandler(error); |
| } |
| return nil; |
| } |
| |
| // If the request is of type ObjectReleaseRequest then don't perform any of the |
| // protocol (check if channel is alive, send ping message, report errors, etc.). If the message |
| // wasn't able to sent then it's most likely that the host side is dead and there's no need |
| // to retry or try to handle it. |
| if ([request class] == [EDOObjectReleaseRequest class]) { |
| [stats reportReleaseObject]; |
| NSData *requestData = [NSKeyedArchiver edo_archivedDataWithObject:request]; |
| [channel sendData:requestData withCompletionHandler:nil]; |
| [EDOChannelPool.sharedChannelPool addChannel:channel forPort:port]; |
| return nil; |
| } else { |
| uint64_t requestStartTime = mach_absolute_time(); |
| __block NSData *responseData = nil; |
| NSData *requestData = [NSKeyedArchiver edo_archivedDataWithObject:request]; |
| |
| if (executor) { |
| // if the current queue has a pending request, send it over. |
| [executor loopWithBlock:^{ |
| responseData = [self sendRequestData:requestData withChannel:channel]; |
| }]; |
| } else { |
| responseData = [self sendRequestData:requestData withChannel:channel]; |
| } |
| |
| EDOServiceResponse *response; |
| Class errorResponseClass = [EDOErrorResponse class]; |
| if (responseData) { |
| response = [NSKeyedUnarchiver edo_unarchiveObjectWithData:responseData]; |
| NSAssert([request.messageID isEqualToString:response.messageID] || |
| [response isKindOfClass:errorResponseClass], |
| @"The response (%@) Id is mismatched with the request (%@)", response, request); |
| } |
| |
| [stats reportRequestType:[request class] |
| requestDuration:EDOGetMillisecondsSinceMachTime(requestStartTime) |
| responseDuration:response.duration]; |
| if (response) { |
| [EDOChannelPool.sharedChannelPool addChannel:channel forPort:port]; |
| if ([response isKindOfClass:errorResponseClass]) { |
| // We raise an exception here for now, but the caller should also be able to handle the |
| // error responses. We will refactor this with a better error reporting logic. |
| EDOErrorResponse *errorResponse = (EDOErrorResponse *)response; |
| NSString *reason = |
| [NSString stringWithFormat:@"eDO call failed at server side, underlying error:\n%@", |
| errorResponse.error.description]; |
| [[self exceptionWithReason:reason port:port error:errorResponse.error] raise]; |
| } |
| return response; |
| } else { |
| // Cleanup broken channels before retry. |
| [EDOChannelPool.sharedChannelPool removeChannelsWithPort:port]; |
| currentAttempt += 1; |
| } |
| } |
| } |
| |
| NSString *description = @"The remote service may be unresponsive due to a crash or hang. Check " |
| @"full logs for more information."; |
| NSDictionary<NSErrorUserInfoKey, id> *userInfo = @{ |
| EDOErrorPortKey : port, |
| EDOErrorRequestKey : request.description, |
| NSLocalizedDescriptionKey : description, |
| }; |
| NSError *error = [NSError errorWithDomain:EDOServiceErrorDomain |
| code:EDOServiceErrorConnectTimeout |
| userInfo:userInfo]; |
| if (errorOut != NULL) { |
| *errorOut = error; |
| } else { |
| gEDOClientErrorHandler(error); |
| } |
| return nil; |
| } |
| |
| #pragma mark - Private |
| |
| /** |
| * Sends EDOObjectAliveRequest to the service that the given object belongs to in the current |
| * process and check it is still alive. |
| * |
| * @param object The remote object to check if it is alive. |
| * @return The underlying object if it is still alive, otherwise @c nil. |
| */ |
| + (id)resolveInstanceFromEDOObject:(EDOObject *)object { |
| @try { |
| EDOObjectAliveRequest *request = [EDOObjectAliveRequest requestWithObject:object]; |
| EDOObjectAliveResponse *response = (EDOObjectAliveResponse *)[EDOClientService |
| sendSynchronousRequest:request |
| onPort:object.servicePort.hostPort]; |
| |
| return response.alive ? (__bridge id)(void *)object.remoteAddress : nil; |
| } @catch (NSException *e) { |
| // In case of the service is dead or error, ignore the exception and reset to nil. |
| return nil; |
| } |
| } |
| |
| + (NSException *)exceptionWithReason:(NSString *)reason |
| port:(EDOHostPort *)port |
| error:(NSError *)error { |
| NSDictionary<NSErrorUserInfoKey, id> *userInfo; |
| if (error) { |
| userInfo = @{EDOExceptionPortKey : port, EDOExceptionUnderlyingErrorKey : error}; |
| } else { |
| userInfo = @{EDOExceptionPortKey : port}; |
| } |
| return [NSException exceptionWithName:NSDestinationInvalidException |
| reason:reason |
| userInfo:userInfo]; |
| } |
| |
| /** Connects to the host service on the given @c port. */ |
| + (id<EDOChannel>)connectPort:(UInt16)port error:(NSError **)error { |
| return [EDOChannelPool.sharedChannelPool channelWithPort:[EDOHostPort hostPortWithLocalPort:port] |
| error:error]; |
| } |
| |
| /** Sends the request data through the given @c channel and waits for the response synchronously. */ |
| + (NSData *)sendRequestData:(NSData *)requestData withChannel:(id<EDOChannel>)channel { |
| __block NSData *responseData; |
| // The channel is asynchronous and not I/O re-entrant so we chain the sending and receiving, |
| // and capture the response in the callback blocks. |
| [channel sendData:requestData withCompletionHandler:nil]; |
| |
| __block BOOL serviceClosed = NO; |
| dispatch_semaphore_t waitLock = dispatch_semaphore_create(0); |
| EDOChannelReceiveHandler receiveHandler = |
| ^(id<EDOChannel> channel, NSData *data, NSError *error) { |
| responseData = data; |
| serviceClosed = data == nil; |
| dispatch_semaphore_signal(waitLock); |
| }; |
| |
| // Check ping response to make sure channel is healthy. |
| [channel receiveDataWithQueue:dispatch_get_global_queue(qos_class_self(), 0) |
| handler:receiveHandler]; |
| |
| dispatch_time_t timeoutInSeconds = dispatch_time(DISPATCH_TIME_NOW, kPingTimeoutSeconds); |
| long result = dispatch_semaphore_wait(waitLock, timeoutInSeconds); |
| |
| // Continue to receive the response if the ping is received. |
| if ([responseData isEqualToData:EDOClientService.pingMessageData]) { |
| [channel receiveDataWithHandler:receiveHandler]; |
| dispatch_semaphore_wait(waitLock, DISPATCH_TIME_FOREVER); |
| } |
| |
| // The ping hasn't been received, or nothing has been received, timing out. |
| if (result != 0 || serviceClosed) { |
| NSLog(@"The edo channel %@ is broken.", channel); |
| } |
| |
| return responseData; |
| } |
| |
| + (id)responseObjectWithRequest:(EDOServiceRequest *)request onPort:(EDOHostPort *)port { |
| EDOServiceResponse *response = [self sendSynchronousRequest:request onPort:port]; |
| id remoteObject = ((EDOObjectResponse *)response).object; |
| remoteObject = [self unwrappedObjectFromObject:remoteObject]; |
| remoteObject = [self cachedEDOFromObjectUpdateIfNeeded:remoteObject]; |
| return remoteObject; |
| } |
| |
| @end |
| |
| /** |
| * This check is called frequently within the library. The shared function caches the checked |
| * classes to optimize the check. |
| */ |
| BOOL EDOIsRemoteObject(id object) { |
| static Class remoteObjectClass; |
| static Class remoteBlockClass; |
| static dispatch_once_t onceToken; |
| dispatch_once(&onceToken, ^{ |
| remoteObjectClass = [EDOObject class]; |
| remoteBlockClass = [EDOBlockObject class]; |
| }); |
| Class objectClass = [object class]; |
| return objectClass == remoteObjectClass || objectClass == remoteBlockClass; |
| } |
| |
| void EDOExportEDOClientError(NSError *error) { gEDOClientErrorHandler(error); } |