blob: e00f80c1fee76de002a1a0e9638e9b0ba9c05950 [file] [log] [blame]
//
// 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); }