From 1592e3919cab20acd73c53b29152596f3fab4393 Mon Sep 17 00:00:00 2001 From: mlch911 Date: Wed, 26 Jul 2023 14:13:08 +0800 Subject: [PATCH 01/13] Feature: Wireless Connection --- LookinServer.podspec | 4 +- LookinShared.podspec | 3 + .../Server/Connection/LKS_ConnectionManager.h | 8 + .../Server/Connection/LKS_ConnectionManager.m | 123 +++- .../Channel/Bonjour/ECONetServiceBrowser.h | 19 + .../Channel/Bonjour/ECONetServiceBrowser.m | 126 ++++ .../Channel/Bonjour/ECONetServicePublisher.h | 15 + .../Channel/Bonjour/ECONetServicePublisher.m | 50 ++ Src/Main/Shared/Channel/ECOBaseChannel.h | 57 ++ Src/Main/Shared/Channel/ECOBaseChannel.m | 33 ++ Src/Main/Shared/Channel/ECOChannelAppInfo.h | 27 + Src/Main/Shared/Channel/ECOChannelAppInfo.m | 85 +++ .../Shared/Channel/ECOChannelDeviceInfo.h | 48 ++ .../Shared/Channel/ECOChannelDeviceInfo.m | 205 +++++++ Src/Main/Shared/Channel/ECOChannelManager.h | 69 +++ Src/Main/Shared/Channel/ECOChannelManager.m | 116 ++++ Src/Main/Shared/Channel/ECOSocketChannel.h | 60 ++ Src/Main/Shared/Channel/ECOSocketChannel.m | 546 ++++++++++++++++++ Src/Main/Shared/Channel/ECOUSBChannel.h | 29 + Src/Main/Shared/Channel/ECOUSBChannel.m | 325 +++++++++++ Src/Main/Shared/LookinAppInfo.h | 2 + Src/Main/Shared/LookinAppInfo.m | 8 + Src/Main/Shared/LookinDefines.h | 9 + 23 files changed, 1952 insertions(+), 15 deletions(-) create mode 100644 Src/Main/Shared/Channel/Bonjour/ECONetServiceBrowser.h create mode 100644 Src/Main/Shared/Channel/Bonjour/ECONetServiceBrowser.m create mode 100644 Src/Main/Shared/Channel/Bonjour/ECONetServicePublisher.h create mode 100644 Src/Main/Shared/Channel/Bonjour/ECONetServicePublisher.m create mode 100644 Src/Main/Shared/Channel/ECOBaseChannel.h create mode 100644 Src/Main/Shared/Channel/ECOBaseChannel.m create mode 100644 Src/Main/Shared/Channel/ECOChannelAppInfo.h create mode 100644 Src/Main/Shared/Channel/ECOChannelAppInfo.m create mode 100644 Src/Main/Shared/Channel/ECOChannelDeviceInfo.h create mode 100644 Src/Main/Shared/Channel/ECOChannelDeviceInfo.m create mode 100644 Src/Main/Shared/Channel/ECOChannelManager.h create mode 100644 Src/Main/Shared/Channel/ECOChannelManager.m create mode 100644 Src/Main/Shared/Channel/ECOSocketChannel.h create mode 100644 Src/Main/Shared/Channel/ECOSocketChannel.m create mode 100644 Src/Main/Shared/Channel/ECOUSBChannel.h create mode 100644 Src/Main/Shared/Channel/ECOUSBChannel.m diff --git a/LookinServer.podspec b/LookinServer.podspec index c5ce5fc7..ceddeab5 100644 --- a/LookinServer.podspec +++ b/LookinServer.podspec @@ -13,7 +13,9 @@ Pod::Spec.new do |spec| spec.source = { :git => "https://github.com/QMUI/LookinServer.git", :tag => "1.2.8"} spec.framework = "UIKit" spec.requires_arc = true - + + spec.dependency 'CocoaAsyncSocket' + spec.subspec 'Core' do |ss| ss.source_files = ['Src/Main/**/*', 'Src/Base/**/*'] ss.pod_target_xcconfig = { diff --git a/LookinShared.podspec b/LookinShared.podspec index 85080837..d864d725 100644 --- a/LookinShared.podspec +++ b/LookinShared.podspec @@ -17,6 +17,9 @@ Pod::Spec.new do |spec| 'Src/Main/Shared/**/*', 'Src/Base/**/*' ] + + spec.dependency 'CocoaAsyncSocket' + spec.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) SHOULD_COMPILE_LOOKIN_SERVER=1' } diff --git a/Src/Main/Server/Connection/LKS_ConnectionManager.h b/Src/Main/Server/Connection/LKS_ConnectionManager.h index 684127b9..fc71cf0b 100644 --- a/Src/Main/Server/Connection/LKS_ConnectionManager.h +++ b/Src/Main/Server/Connection/LKS_ConnectionManager.h @@ -20,6 +20,14 @@ extern NSString *const LKS_ConnectionDidEndNotificationName; @property(nonatomic, assign) BOOL applicationIsActive; +- (void)startWirelessConnection; + +- (void)endWirelessConnection; + +- (BOOL)isConnected; + +- (BOOL)isWirelessConnnect; + - (void)respond:(LookinConnectionResponseAttachment *)data requestType:(uint32_t)requestType tag:(uint32_t)tag; - (void)pushData:(NSObject *)data type:(uint32_t)type; diff --git a/Src/Main/Server/Connection/LKS_ConnectionManager.m b/Src/Main/Server/Connection/LKS_ConnectionManager.m index bcc2dbc5..35630b5f 100644 --- a/Src/Main/Server/Connection/LKS_ConnectionManager.m +++ b/Src/Main/Server/Connection/LKS_ConnectionManager.m @@ -16,6 +16,9 @@ #import "LookinServerDefines.h" #import "LKS_TraceManager.h" #import "LKS_MultiplatformAdapter.h" +#import "ECOChannelManager.h" + +@import CocoaAsyncSocket; NSString *const LKS_ConnectionDidEndNotificationName = @"LKS_ConnectionDidEndNotificationName"; @@ -25,6 +28,11 @@ @interface LKS_ConnectionManager () @property(nonatomic, strong) LKS_RequestHandler *requestHandler; +@property(nonatomic, strong) ECOChannelManager *wirelessChannel; +@property(nonatomic, strong) ECOChannelDeviceInfo *wirelessDevice; + +@property BOOL hasStartWirelessConnnection; + @end @implementation LKS_ConnectionManager @@ -50,12 +58,14 @@ + (void)load { - (instancetype)init { if (self = [super init]) { NSLog(@"LookinServer - Will launch. Framework version: %@", LOOKIN_SERVER_READABLE_VERSION); - + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleApplicationDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleWillResignActiveNotification) name:UIApplicationWillResignActiveNotification object:nil]; - + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleLocalInspect:) name:@"Lookin_2D" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleLocalInspect:) name:@"Lookin_3D" object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(startWirelessConnection) name:@"Lookin_startWirelessConnection" object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(endWirelessConnection) name:@"Lookin_endWirelessConnection" object:nil]; [[NSNotificationCenter defaultCenter] addObserverForName:@"Lookin_Export" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { [[LKS_ExportManager sharedInstance] exportAndShare]; }]; @@ -63,15 +73,90 @@ - (instancetype)init { [[LKS_TraceManager sharedInstance] addSearchTarger:note.object]; }]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleGetLookinInfo:) name:@"GetLookinInfo" object:nil]; - + self.requestHandler = [LKS_RequestHandler new]; } return self; } +- (void)startWirelessConnection { + self.hasStartWirelessConnnection = YES; + if (!self.wirelessChannel) { +#if TARGET_OS_IPHONE + self.wirelessChannel = ECOChannelManager.new; + __weak __typeof(self) weakSelf = self; + // 接收到数据回调 + self.wirelessChannel.receivedBlock = ^(ECOChannelDeviceInfo *device, NSData *data, NSDictionary *extraInfo) { + NSLog(@"🚀 Lookin receivedBlock device:%@", device); + NSNumber *type = extraInfo[@"type"]; + NSNumber *tag = extraInfo[@"tag"]; + id object = nil; + id unarchivedObject = [NSKeyedUnarchiver unarchiveObjectWithData:data]; + if ([unarchivedObject isKindOfClass:[LookinConnectionAttachment class]]) { + LookinConnectionAttachment *attachment = (LookinConnectionAttachment *)unarchivedObject; + object = attachment.data; + } else { + object = unarchivedObject; + } + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf.requestHandler handleRequestType:type.intValue tag:tag.intValue object:object]; + }); + }; + // 设备连接变更 + self.wirelessChannel.deviceBlock = ^(ECOChannelDeviceInfo *device, BOOL isConnected) { + NSLog(@"🚀 Lookin deviceBlock device:%@", device); + if ([device isEqual:weakSelf.wirelessDevice] && !isConnected) { + weakSelf.wirelessDevice = nil; + } + }; + // 授权状态变更回调 + self.wirelessChannel.authStateChangedBlock = ^(ECOChannelDeviceInfo *device, ECOAuthorizeResponseType authState) { + NSLog(@"🚀 Lookin authStateChangedBlock device:%@ authState:%ld", device, authState); + }; + // 请求授权状态认证回调 + self.wirelessChannel.requestAuthBlock = ^(ECOChannelDeviceInfo *device, ECOAuthorizeResponseType authState) { + NSLog(@"🚀 Lookin requestAuthBlock device:%@ authState:%ld", device, authState); + NSString *title = @"Lookin 连接请求"; + NSString *message = [NSString stringWithFormat:@"%@ 的Lookin想要连接你的设备,如果你想启用调试功能,请选择允许", device.hostName ?: device.ipAddress]; + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *denyAction = [UIAlertAction actionWithTitle:@"拒绝" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { + [weakSelf.wirelessChannel sendAuthorizationMessageToDevice:device state:ECOAuthorizeResponseType_Deny showAuthAlert:NO]; + }]; + UIAlertAction *allowOnceAction = [UIAlertAction actionWithTitle:@"允许一次" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { + [weakSelf.wirelessChannel sendAuthorizationMessageToDevice:device state:ECOAuthorizeResponseType_AllowOnce showAuthAlert:NO]; + weakSelf.wirelessDevice = device; + }]; + UIAlertAction *allowAlwaysAction = [UIAlertAction actionWithTitle:@"始终允许" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { + [weakSelf.wirelessChannel sendAuthorizationMessageToDevice:device state:ECOAuthorizeResponseType_AllowAlways showAuthAlert:NO]; + weakSelf.wirelessDevice = device; + }]; + [alertController addAction:denyAction]; + [alertController addAction:allowOnceAction]; + [alertController addAction:allowAlwaysAction]; + + dispatch_async(dispatch_get_main_queue(), ^{ + UIViewController *rootVC = [UIApplication sharedApplication].keyWindow.rootViewController; + [rootVC presentViewController:alertController animated:YES completion:nil]; + }); + }; +#endif + } +} + +- (void)endWirelessConnection { + self.hasStartWirelessConnnection = NO; + GCDAsyncSocket *asyncSocket = [self.wirelessChannel valueForKeyPath:@"socketChannel.cSocket"]; + if (asyncSocket) { + [asyncSocket setDelegate:nil]; + [asyncSocket disconnect]; + [self.wirelessChannel setValue:nil forKeyPath:@"socketChannel.cSocket"]; + } + self.wirelessChannel = nil; +} + - (void)_handleWillResignActiveNotification { self.applicationIsActive = NO; - + if (self.peerChannel_ && ![self.peerChannel_ isConnected]) { [self.peerChannel_ close]; self.peerChannel_ = nil; @@ -91,7 +176,7 @@ - (void)searchPortToListenIfNoConnection { NSLog(@"LookinServer - Searching port to listen..."); [self.peerChannel_ close]; self.peerChannel_ = nil; - + if ([self isiOSAppOnMac]) { [self _tryToListenOnPortFrom:LookinSimulatorIPv4PortNumberStart to:LookinSimulatorIPv4PortNumberEnd current:LookinSimulatorIPv4PortNumberStart]; } else { @@ -130,7 +215,7 @@ - (void)_tryToListenOnPortFrom:(int)fromPort to:(int)toPort current:(int)current } else { // 未知失败 } - + if (currentPort < toPort) { // 尝试下一个端口 NSLog(@"LookinServer - 127.0.0.1:%d is unavailable(%@). Will try anothor address ...", currentPort, error); @@ -140,7 +225,7 @@ - (void)_tryToListenOnPortFrom:(int)fromPort to:(int)toPort current:(int)current NSLog(@"LookinServer - 127.0.0.1:%d is unavailable(%@).", currentPort, error); NSLog(@"LookinServer - Connect failed in the end."); } - + } else { // 成功 NSLog(@"LookinServer - Connected successfully on 127.0.0.1:%d", currentPort); @@ -157,6 +242,14 @@ - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } +- (BOOL)isConnected { + return self.isWirelessConnnect || (self.peerChannel_ && self.peerChannel_.isConnected); +} + +- (BOOL)isWirelessConnnect { + return self.wirelessChannel.isConnected; +} + - (void)respond:(LookinConnectionResponseAttachment *)data requestType:(uint32_t)requestType tag:(uint32_t)tag { [self _sendData:data frameOfType:requestType tag:tag]; } @@ -166,15 +259,17 @@ - (void)pushData:(NSObject *)data type:(uint32_t)type { } - (void)_sendData:(NSObject *)data frameOfType:(uint32_t)frameOfType tag:(uint32_t)tag { + NSData *archivedData = [NSKeyedArchiver archivedDataWithRootObject:data]; if (self.peerChannel_) { - NSData *archivedData = [NSKeyedArchiver archivedDataWithRootObject:data]; dispatch_data_t payload = [archivedData createReferencingDispatchData]; - + [self.peerChannel_ sendFrameOfType:frameOfType tag:tag withPayload:payload callback:^(NSError *error) { if (error) { } }]; - } + } else if (self.wirelessDevice.isConnected) { + [self.wirelessChannel sendPacket:archivedData extraInfo:@{@"tag": @(tag), @"type": @(frameOfType)} toDevice:self.wirelessDevice]; + } } #pragma mark - Lookin_PTChannelDelegate @@ -209,10 +304,10 @@ - (void)ioFrameChannel:(Lookin_PTChannel*)channel didAcceptConnection:(Lookin_PT NSLog(@"LookinServer - channel:%@, acceptConnection:%@", channel.debugTag, otherChannel.debugTag); Lookin_PTChannel *previousChannel = self.peerChannel_; - + otherChannel.targetPort = address.port; self.peerChannel_ = otherChannel; - + [previousChannel cancel]; } @@ -225,7 +320,7 @@ - (void)ioFrameChannel:(Lookin_PTChannel*)channel didEndWithError:(NSError*)erro } // Client 端关闭时,会走到这里 NSLog(@"LookinServer - channel%@ DidEndWithError:%@", channel.debugTag, error); - + [[NSNotificationCenter defaultCenter] postNotificationName:LKS_ConnectionDidEndNotificationName object:self]; [self searchPortToListenIfNoConnection]; } @@ -239,7 +334,7 @@ - (void)_handleLocalInspect:(NSNotification *)note { UIWindow *keyWindow = [LKS_MultiplatformAdapter keyWindow]; UIViewController *rootViewController = [keyWindow rootViewController]; [rootViewController presentViewController:alertController animated:YES completion:nil]; - + NSLog(@"LookinServer - Failed to run local inspection. The feature has been removed. Please use the computer version of Lookin or consider SDKs like FLEX for similar functionality."); } diff --git a/Src/Main/Shared/Channel/Bonjour/ECONetServiceBrowser.h b/Src/Main/Shared/Channel/Bonjour/ECONetServiceBrowser.h new file mode 100644 index 00000000..f8f5fbd7 --- /dev/null +++ b/Src/Main/Shared/Channel/Bonjour/ECONetServiceBrowser.h @@ -0,0 +1,19 @@ +// +// ECONetServiceBrowser.h +// Echo +// +// Created by 陈爱彬 on 2019/4/17. Maintain by 陈爱彬 +// Description +// + +#import + +typedef void(^ECONetServiceBrowserResolvedAddressesBlock)(NSArray *addresses, NSString *hostName); + +@interface ECONetServiceBrowser : NSObject + +@property (nonatomic, copy) ECONetServiceBrowserResolvedAddressesBlock addressesBlock; + +- (void)startBrowsing; + +@end diff --git a/Src/Main/Shared/Channel/Bonjour/ECONetServiceBrowser.m b/Src/Main/Shared/Channel/Bonjour/ECONetServiceBrowser.m new file mode 100644 index 00000000..983615d0 --- /dev/null +++ b/Src/Main/Shared/Channel/Bonjour/ECONetServiceBrowser.m @@ -0,0 +1,126 @@ +// +// ECONetServiceBrowser.m +// Echo +// +// Created by 陈爱彬 on 2019/4/17. Maintain by 陈爱彬 +// Description +// + +#import "ECONetServiceBrowser.h" +#import "LookinDefines.h" +#if TARGET_OS_IPHONE +#import +#endif + +@interface ECONetServiceBrowser() + + +@property (nonatomic, strong) NSMutableArray *services; +@property (nonatomic, strong) NSNetServiceBrowser *serviceBrowser; + +@end + +@implementation ECONetServiceBrowser + +- (instancetype)init { + self = [super init]; + if (self) { + + } + return self; +} +#pragma mark - Browser Services +//启动Bonjour服务搜索 +- (void)startBrowsing { + [self.services removeAllObjects]; + //创建Browser对象 + self.serviceBrowser = [[NSNetServiceBrowser alloc] init]; + self.serviceBrowser.includesPeerToPeer = YES; + [self.serviceBrowser setDelegate:self]; + [self.serviceBrowser searchForServicesOfType:LookinNetServiceType inDomain:LookinNetServiceDomain]; +} +//重置查找服务 +- (void)resetBrowserService { + [self.serviceBrowser stop]; + self.serviceBrowser = nil; + //重启查找 + [self startBrowsing]; +} +#pragma mark - NSNetServiceBrowserDelegate methods + +/* Sent to the NSNetServiceBrowser instance's delegate for each service discovered. If there are more services, moreComing will be YES. If for some reason handling discovered services requires significant processing, accumulating services until moreComing is NO and then doing the processing in bulk fashion may be desirable. + */ +- (void)netServiceBrowser:(NSNetServiceBrowser *)browser didFindService:(NSNetService *)service moreComing:(BOOL)moreComing { + NSLog(@"%s",__func__); + //解析服务 + [self.services addObject:service]; + service.delegate = self; + [service resolveWithTimeout:LookinNetServiceResolveAddressTimeout]; +} + +/* Sent to the NSNetServiceBrowser instance's delegate when a previously discovered service is no longer published. + */ +- (void)netServiceBrowser:(NSNetServiceBrowser *)browser didRemoveService:(NSNetService *)service moreComing:(BOOL)moreComing { + NSLog(@"%s",__func__); + //移除服务 + [self.services removeObject:service]; + service.delegate = nil; +} + +/* Sent to the NSNetServiceBrowser instance's delegate when an error in searching for domains or services has occurred. The error dictionary will contain two key/value pairs representing the error domain and code (see the NSNetServicesError enumeration above for error code constants). It is possible for an error to occur after a search has been started successfully. + */ +- (void)netServiceBrowser:(NSNetServiceBrowser *)browser didNotSearch:(NSDictionary *)errorDict { + NSLog(@"%s",__func__); + //重试 +#if TARGET_OS_IPHONE + if (@available(iOS 14.0, *)) { + NSNetServicesError errorCode = [errorDict[@"NSNetServicesErrorCode"] integerValue]; + if (errorCode == -72008) { + //iOS14新增本地网络隐私权限,提示用户如何设置并忽略 + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + NSString *title = @"Echo 连接提示"; + NSString *message = @"由于iOS14本地网络权限限制,请在Info.plist中设置NSLocalNetworkUsageDescription和NSBonjourServices,详细内容见:https://github.com/didi/echo"; + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"好的" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { + + }]; + [alertController addAction:confirmAction]; + UIViewController *rootVC = [UIApplication sharedApplication].keyWindow.rootViewController; + [rootVC presentViewController:alertController animated:YES completion:nil]; + }); + NSLog(@">>Echo Warning:Bonjour服务错误,由于iOS14本地网络权限限制,请在Info.plist中设置NSLocalNetworkUsageDescription和NSBonjourServices,详细内容见:https://github.com/didi/echo"); + return; + } + } +#endif + [self resetBrowserService]; +} +#pragma mark - NSNetServiceDelegate methods + +/* Sent to the NSNetService instance's delegate when one or more addresses have been resolved for an NSNetService instance. Some NSNetService methods will return different results before and after a successful resolution. An NSNetService instance may get resolved more than once; truly robust clients may wish to resolve again after an error, or to resolve more than once. + */ +- (void)netServiceDidResolveAddress:(NSNetService *)sender { + NSLog(@"%s",__func__); + //解析address地址 + NSString *name = [sender name]; + NSString *hostName = name ?: [sender hostName]; + NSArray *addresses = [[sender addresses] copy]; + !self.addressesBlock ?: self.addressesBlock(addresses, hostName ?: @""); +} + +/* Sent to the NSNetService instance's delegate when the instance's previously running publication or resolution request has stopped. + */ +- (void)netServiceDidStop:(NSNetService *)sender { + NSLog(@"%s",__func__); +} + +#pragma mark - getters +- (NSMutableArray *)services { + if (!_services) { + _services = [[NSMutableArray alloc] initWithCapacity:0]; + } + return _services; +} + +@end diff --git a/Src/Main/Shared/Channel/Bonjour/ECONetServicePublisher.h b/Src/Main/Shared/Channel/Bonjour/ECONetServicePublisher.h new file mode 100644 index 00000000..d215e54d --- /dev/null +++ b/Src/Main/Shared/Channel/Bonjour/ECONetServicePublisher.h @@ -0,0 +1,15 @@ +// +// ECONetServicePublisher.h +// Echo +// +// Created by 陈爱彬 on 2019/4/17. Maintain by 陈爱彬 +// Description +// + +#import + +@interface ECONetServicePublisher : NSObject + +- (void)startPublish; + +@end diff --git a/Src/Main/Shared/Channel/Bonjour/ECONetServicePublisher.m b/Src/Main/Shared/Channel/Bonjour/ECONetServicePublisher.m new file mode 100644 index 00000000..0812bffd --- /dev/null +++ b/Src/Main/Shared/Channel/Bonjour/ECONetServicePublisher.m @@ -0,0 +1,50 @@ +// +// ECONetServicePublisher.m +// Echo +// +// Created by 陈爱彬 on 2019/4/17. Maintain by 陈爱彬 +// Description +// + +#import "ECONetServicePublisher.h" +#import "LookinDefines.h" + +@interface ECONetServicePublisher() + + +@property (nonatomic, strong) NSNetService *netService; +@end + +@implementation ECONetServicePublisher + +- (instancetype)init { + self = [super init]; + if (self) { + + } + return self; +} +#pragma mark - Publish Service +- (void)startPublish { + if (self.netService) { + [self.netService stop]; + self.netService.delegate = nil; + self.netService = nil; + } + self.netService = [[NSNetService alloc] initWithDomain:LookinNetServiceDomain type:LookinNetServiceType name:LookinNetServiceName port:LookinNetServicePortNumber]; + self.netService.delegate = self; + [self.netService publish]; +} +#pragma mark - NSNetServiceDelegate methods +/* Sent to the NSNetService instance's delegate when the publication of the instance is complete and successful. + */ +- (void)netServiceDidPublish:(NSNetService *)sender { + +} +/* Sent to the NSNetService instance's delegate when an error in publishing the instance occurs. The error dictionary will contain two key/value pairs representing the error domain and code (see the NSNetServicesError enumeration above for error code constants). It is possible for an error to occur after a successful publication. + */ +- (void)netService:(NSNetService *)sender didNotPublish:(NSDictionary *)errorDict { + +} + +@end diff --git a/Src/Main/Shared/Channel/ECOBaseChannel.h b/Src/Main/Shared/Channel/ECOBaseChannel.h new file mode 100644 index 00000000..e39c6cb0 --- /dev/null +++ b/Src/Main/Shared/Channel/ECOBaseChannel.h @@ -0,0 +1,57 @@ +// +// ECOBaseChannel.h +// Echo +// +// Created by 陈爱彬 on 2019/4/18. Maintain by 陈爱彬 +// Description +// + +#import +#import "ECOChannelDeviceInfo.h" + +// 通道优先级,usb > socket +typedef NS_ENUM(NSInteger, ECOChannelPriority) { + ECOChannelPriority_Socket = 0, + ECOChannelPriority_USB, +}; + +typedef NS_ENUM(NSInteger, ECOChannelConnectType) { + ECOChannelConnectType_Authorization = 0, //授权连接 + ECOChannelConnectType_Auto, //自动连接 +}; + +@class ECOBaseChannel; +@protocol ECOChannelConnectedDeviceProtocol + +@optional +//新的设备连接 +- (void)channel:(ECOBaseChannel *)channel didConnectedToDevice:(ECOChannelDeviceInfo *)device; +//设备已断开 +- (void)channel:(ECOBaseChannel *)channel didDisconnectWithDevice:(ECOChannelDeviceInfo *)device; +//接收到数据 +- (void)channel:(ECOBaseChannel *)channel didReceivedDevice:(ECOChannelDeviceInfo *)device andData:(NSData *)data extraInfo:(NSDictionary *)extraInfo; + +//设备变更了授权状态 +- (void)channel:(ECOBaseChannel *)channel device:(ECOChannelDeviceInfo *)device didChangedAuthState:(ECOAuthorizeResponseType)authorizedType; + +//设备要请求授权状态,弹窗展示 +- (void)channel:(ECOBaseChannel *)channel device:(ECOChannelDeviceInfo *)device willRequestAuthState:(ECOAuthorizeResponseType)authorizedType; + +@end + +@interface ECOBaseChannel : NSObject + +@property (nonatomic, assign) ECOChannelPriority priority; +@property (nonatomic, weak) id delegate; +@property (nonatomic, assign) ECOChannelConnectType connectType; + +- (instancetype)initWithDelegate:(id)delegate; +- (instancetype)init NS_UNAVAILABLE; + +//初始化Channel,供子类调用,该方法会在初始化后自动被调用 +- (void)setupChannel; + +// 是否已连接到Mac客户端 +- (BOOL)isConnected; + +@end diff --git a/Src/Main/Shared/Channel/ECOBaseChannel.m b/Src/Main/Shared/Channel/ECOBaseChannel.m new file mode 100644 index 00000000..3e57fc69 --- /dev/null +++ b/Src/Main/Shared/Channel/ECOBaseChannel.m @@ -0,0 +1,33 @@ +// +// ECOBaseChannel.m +// Echo +// +// Created by 陈爱彬 on 2019/4/18. Maintain by 陈爱彬 +// Description +// + +#import "ECOBaseChannel.h" + +@implementation ECOBaseChannel + +- (instancetype)initWithDelegate:(id)delegate { + self = [super init]; + if (self) { + self.delegate = delegate; + //目前使用授权连接机制,如果想自动连接已发现的设备,将该值改为ECOChannelConnectType_Auto + self.connectType = ECOChannelConnectType_Authorization; + [self setupChannel]; + } + return self; +} + +//初始化Channel,供子类调用,该方法会在初始化后自动被调用 +- (void)setupChannel { + +} + +// 是否已连接到Mac客户端 +- (BOOL)isConnected { + return NO; +} +@end diff --git a/Src/Main/Shared/Channel/ECOChannelAppInfo.h b/Src/Main/Shared/Channel/ECOChannelAppInfo.h new file mode 100644 index 00000000..d410e209 --- /dev/null +++ b/Src/Main/Shared/Channel/ECOChannelAppInfo.h @@ -0,0 +1,27 @@ +// +// ECOChannelAppInfo.h +// EchoSDK +// +// Created by 陈爱彬 on 2019/10/30. Maintain by 陈爱彬 +// Description +// + +#import + +@interface ECOChannelAppInfo : NSObject + +@property (nonatomic, copy) NSString *appId; +@property (nonatomic, copy) NSString *appName; +@property (nonatomic, copy) NSString *appShortVersion; +@property (nonatomic, copy) NSString *appVersion; +@property (nonatomic, copy) NSString *appIcon; + +- (instancetype)initWithDictionary:(NSDictionary *)dict; + +- (NSDictionary *)toDictionary; + +//由外部设置通用的appid和appname ++ (void)setUniqueAppId:(NSString *)appId + appName:(NSString *)appName; + +@end diff --git a/Src/Main/Shared/Channel/ECOChannelAppInfo.m b/Src/Main/Shared/Channel/ECOChannelAppInfo.m new file mode 100644 index 00000000..df68ddfa --- /dev/null +++ b/Src/Main/Shared/Channel/ECOChannelAppInfo.m @@ -0,0 +1,85 @@ +// +// ECOChannelAppInfo.m +// EchoSDK +// +// Created by 陈爱彬 on 2019/10/30. Maintain by 陈爱彬 +// Description +// + +#import "ECOChannelAppInfo.h" + +static NSString *_ecoUniqueAppId = nil; +static NSString *_ecoUniqueAppName = nil; + +@implementation ECOChannelAppInfo + +- (instancetype)init { + self = [super init]; + if (self) { + if (_ecoUniqueAppId) { + self.appId = _ecoUniqueAppId; + }else{ + self.appId = [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString*)kCFBundleIdentifierKey]; + } + if (_ecoUniqueAppName) { + self.appName = _ecoUniqueAppName; + }else{ + NSString *displayName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"]; + NSString *bundleName = [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString*)kCFBundleNameKey]; + self.appName = displayName ?: bundleName; + } + self.appShortVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"] ?: @""; + self.appVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"] ?: @""; +#if TARGET_OS_IPHONE + NSString *icon = [[[[NSBundle mainBundle] infoDictionary] valueForKeyPath:@"CFBundleIcons.CFBundlePrimaryIcon.CFBundleIconFiles"] lastObject]; + UIImage *appIcon = [UIImage imageNamed:icon]; + if (appIcon) { + NSData *iconData = UIImageJPEGRepresentation(appIcon, 1.0f); + NSString *encodedImageStr = [iconData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; + self.appIcon = encodedImageStr; + } +#endif + } + return self; +} + +- (instancetype)initWithDictionary:(NSDictionary *)dict { + self = [super init]; + if (self) { + self.appId = dict[@"appId"] ?: @""; + self.appName = dict[@"appName"] ?: @""; + self.appShortVersion = dict[@"sVer"] ?: @""; + self.appVersion = dict[@"ver"] ?: @""; + self.appIcon = dict[@"appIcon"] ?: @""; + } + return self; +} + +- (NSDictionary *)toDictionary { + return @{@"appId": self.appId ?: @"", + @"appName": self.appName ?: @"", + @"sVer": self.appShortVersion ?: @"", + @"ver": self.appVersion ?: @"", + @"appIcon": self.appIcon ?: @"" + }; +} + +- (BOOL)isEqual:(id)object { + if (![object isKindOfClass:[ECOChannelAppInfo class]]) { + return NO; + } + ECOChannelAppInfo *project = (ECOChannelAppInfo *)object; + if (![project.appId isEqual:self.appId]) { + return NO; + } + return YES; +} + +//由外部设置通用的appid和appname ++ (void)setUniqueAppId:(NSString *)appId + appName:(NSString *)appName { + _ecoUniqueAppId = appId; + _ecoUniqueAppName = appName; +} + +@end diff --git a/Src/Main/Shared/Channel/ECOChannelDeviceInfo.h b/Src/Main/Shared/Channel/ECOChannelDeviceInfo.h new file mode 100644 index 00000000..66ffb13a --- /dev/null +++ b/Src/Main/Shared/Channel/ECOChannelDeviceInfo.h @@ -0,0 +1,48 @@ +// +// ECOChannelDeviceInfo.h +// Echo +// +// Created by 陈爱彬 on 2019/4/17. Maintain by 陈爱彬 +// Description 设备信息 +// + +#import +#import "ECOChannelAppInfo.h" + +typedef NS_ENUM(NSInteger, ECODeviceType) { + ECODeviceType_Simulator = 0, //模拟器 + ECODeviceType_Device, //真机 + ECODeviceType_MacApp, //Mac客户端 +}; + +typedef NS_ENUM(NSInteger, ECOAuthorizeResponseType) { + ECOAuthorizeResponseType_Deny = 0, //拒绝连接 + ECOAuthorizeResponseType_AllowOnce, //允许本次 + ECOAuthorizeResponseType_AllowAlways, //始终允许 +}; + +@interface ECOChannelDeviceInfo : NSObject + +@property (nonatomic, copy) NSString *hostName; +@property (nonatomic, copy, readonly) NSString *ipAddress; +@property (nonatomic, copy, readonly) NSString *uuid; +@property (nonatomic, copy, readonly) NSString *deviceName; +@property (nonatomic, copy, readonly) NSString *systemVersion; +@property (nonatomic, copy, readonly) NSString *deviceModel; +@property (nonatomic, assign, readonly) ECODeviceType deviceType; +@property (nonatomic, assign) ECOAuthorizeResponseType authorizedType; +@property (nonatomic, assign) BOOL showAuthAlert; +@property (nonatomic, assign) BOOL isConnected; +@property (nonatomic, strong, readonly) ECOChannelAppInfo *appInfo; + +/** + 透传数据 + */ +@property (nonatomic, copy) NSDictionary *extraData; + +//解析网络信息传输过来的设备信息 +- (instancetype)initWithData:(NSData *)data; + +- (NSDictionary *)toJSONObject; + +@end diff --git a/Src/Main/Shared/Channel/ECOChannelDeviceInfo.m b/Src/Main/Shared/Channel/ECOChannelDeviceInfo.m new file mode 100644 index 00000000..45fa6fbf --- /dev/null +++ b/Src/Main/Shared/Channel/ECOChannelDeviceInfo.m @@ -0,0 +1,205 @@ +// +// ECOChannelDeviceInfo.m +// Echo +// +// Created by 陈爱彬 on 2019/4/17. Maintain by 陈爱彬 +// Description +// + +#import "ECOChannelDeviceInfo.h" +#include +#include +#include +#include + +static NSInteger const ECOINET_ADDRSTRLEN = 16; +static NSInteger const ECOINET6_ADDRSTRLEN = 46; +static NSString *_macUUIDString = nil; + +@interface ECOChannelDeviceInfo() + +@property (nonatomic, readwrite) NSString *ipAddress; +@property (nonatomic, readwrite) NSString *uuid; +@property (nonatomic, readwrite) NSString *deviceName; +@property (nonatomic, readwrite) NSString *systemVersion; +@property (nonatomic, readwrite) NSString *deviceModel; +@property (nonatomic, readwrite) ECODeviceType deviceType; +@property (nonatomic, readwrite) ECOChannelAppInfo *appInfo; + +@end + +@implementation ECOChannelDeviceInfo + +//解析网络信息传输过来的设备信息 +- (instancetype)initWithData:(NSData *)data { + self = [super init]; + if (self) { + NSError *error = nil; + NSDictionary *deviceDict = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + if (deviceDict) { + self.uuid = deviceDict[@"uuid"]; + self.ipAddress = deviceDict[@"ipAddress"]; + self.deviceName = deviceDict[@"deviceName"]; + self.deviceType = [deviceDict[@"deviceType"] integerValue]; + self.systemVersion = deviceDict[@"systemVersion"]; + self.deviceModel = deviceDict[@"model"]; + self.authorizedType = [deviceDict[@"authType"] integerValue]; + self.showAuthAlert = [deviceDict[@"showAuth"] boolValue]; + self.hostName = deviceDict[@"hostName"]; + + ECOChannelAppInfo *appInfo = [[ECOChannelAppInfo alloc] initWithDictionary:deviceDict[@"appInfo"]]; + self.appInfo = appInfo; + } + } + return self; +} + +- (id)copy { + ECOChannelDeviceInfo *deviceInfo = [ECOChannelDeviceInfo new]; + deviceInfo.hostName = self.hostName; + deviceInfo.ipAddress = self.ipAddress; + deviceInfo.uuid = self.uuid; + deviceInfo.deviceName = self.deviceName; + deviceInfo.systemVersion = self.systemVersion; + deviceInfo.deviceModel = self.deviceModel; + deviceInfo.deviceType = self.deviceType; + deviceInfo.authorizedType = self.authorizedType; + deviceInfo.showAuthAlert = self.showAuthAlert; + deviceInfo.appInfo = self.appInfo; + + return deviceInfo; +} + +- (instancetype)init { + self = [super init]; + if (self) { +#if TARGET_OS_OSX + [self setupMacDeviceInfo]; +#else + [self setupIOSDeviceInfo]; +#endif + //ipAddress + NSDictionary *addresses = [self getIPAddresses]; + //这里只取局域网的en0/ipv4地址,ipv6和pdp_ip0/ipv4暂时先忽略 + NSString *address = addresses[@"en0/ipv4"]; + self.ipAddress = address ?: @"0.0.0.0"; + } + return self; +} +- (void)setupMacDeviceInfo { + //uuid + if (!_macUUIDString) { + _macUUIDString = [[NSUUID UUID] UUIDString]; + } + self.uuid = _macUUIDString; + //设备 + self.deviceName = @"MacApp"; + self.deviceType = ECODeviceType_MacApp; +} +#if TARGET_OS_IPHONE +- (void)setupIOSDeviceInfo { + //设备名称 + UIDevice *device = [UIDevice currentDevice]; + self.deviceName = device.name; + self.deviceModel = device.model; + self.systemVersion = device.systemVersion; + //是否为模拟器 +#if TARGET_IPHONE_SIMULATOR + self.deviceType = ECODeviceType_Simulator; +#else + self.deviceType = ECODeviceType_Device; +#endif + //uuid + // self.uuid = [[NSUUID UUID] UUIDString]; + self.uuid = [[device identifierForVendor] UUIDString]; + + ECOChannelAppInfo *appInfo = [ECOChannelAppInfo new]; + self.appInfo = appInfo; +} +#endif +#pragma mark - IPAddress +//获取本机的ip地址表 +- (NSDictionary *)getIPAddresses { + NSMutableDictionary *addresses = [NSMutableDictionary dictionaryWithCapacity:8]; + + // retrieve the current interfaces - returns 0 on success + struct ifaddrs *interfaces; + if(!getifaddrs(&interfaces)) { + // Loop through linked list of interfaces + struct ifaddrs *interface; + for(interface=interfaces; interface; interface=interface->ifa_next) { + if(!(interface->ifa_flags & IFF_UP) /* || (interface->ifa_flags & IFF_LOOPBACK) */ ) { + continue; // deeply nested code harder to read + } + const struct sockaddr_in *addr = (const struct sockaddr_in*)interface->ifa_addr; + char addrBuf[ MAX(ECOINET_ADDRSTRLEN, ECOINET6_ADDRSTRLEN) ]; + if(addr && (addr->sin_family==AF_INET || addr->sin_family==AF_INET6)) { + NSString *name = [NSString stringWithUTF8String:interface->ifa_name]; + NSString *type; + if(addr->sin_family == AF_INET) { + if(inet_ntop(AF_INET, &addr->sin_addr, addrBuf, ECOINET_ADDRSTRLEN)) { + type = @"ipv4"; + } + } else { + const struct sockaddr_in6 *addr6 = (const struct sockaddr_in6*)interface->ifa_addr; + if(inet_ntop(AF_INET6, &addr6->sin6_addr, addrBuf, ECOINET6_ADDRSTRLEN)) { + type = @"ipv6"; + } + } + if(type) { + NSString *key = [NSString stringWithFormat:@"%@/%@", name, type]; + addresses[key] = [NSString stringWithUTF8String:addrBuf]; + } + } + } + // Free memory + freeifaddrs(interfaces); + } + return [addresses count] ? addresses : nil; +} + +#pragma mark - Helper +- (NSDictionary *)toJSONObject { + NSMutableDictionary *json = [NSMutableDictionary dictionary]; + [json setValue:self.uuid ?: @"" forKey:@"uuid"]; + [json setValue:self.ipAddress ?: @"0.0.0.0" forKey:@"ipAddress"]; + [json setValue:self.deviceName ?: @"" forKey:@"deviceName"]; + [json setValue:self.deviceModel ?: @"" forKey:@"model"]; + [json setValue:@(self.deviceType) forKey:@"deviceType"]; + [json setValue:self.systemVersion ?: @"" forKey:@"systemVersion"]; + [json setValue:@(self.authorizedType) forKey:@"authType"]; + [json setValue:@(self.showAuthAlert) forKey:@"showAuth"]; + [json setValue:self.hostName ?: @"" forKey:@"hostName"]; + [json setValue:[self.appInfo toDictionary] forKey:@"appInfo"]; + + return [json copy]; +} +// 返回调试信息 +- (NSString *)description { + return [NSString stringWithFormat:@"%@", @{@"uuid": self.uuid ?: @"", + @"ipAddress": self.ipAddress ?: @"0.0.0.0", + @"deviceName": self.deviceName ?: @"", + @"deviceModel": self.deviceModel ?: @"", + @"deviceType": @(self.deviceType), + @"systemVersion": self.systemVersion ?: @"", + @"authType": @(self.authorizedType), + @"showAuth": @(self.showAuthAlert), + @"appId": self.appInfo.appId ?: @"", + @"appName": self.appInfo.appName ?: @"", + @"hostName": self.hostName ?: @"" + }]; +} + +- (BOOL)isEqual:(id)object { + if (![object isKindOfClass:[ECOChannelDeviceInfo class]]) { + return NO; + } + ECOChannelDeviceInfo *device = (ECOChannelDeviceInfo *)object; + if ([device.uuid isEqualToString:self.uuid] && + [device.appInfo.appId isEqualToString:self.appInfo.appId]) { + return YES; + } + return NO; +} + +@end diff --git a/Src/Main/Shared/Channel/ECOChannelManager.h b/Src/Main/Shared/Channel/ECOChannelManager.h new file mode 100644 index 00000000..fe886be9 --- /dev/null +++ b/Src/Main/Shared/Channel/ECOChannelManager.h @@ -0,0 +1,69 @@ +// +// ECOChannelManager.h +// Echo +// +// Created by 陈爱彬 on 2019/4/16. Maintain by 陈爱彬 +// Description +// + +#import +#import "ECOChannelDeviceInfo.h" +#import "ECOUSBChannel.h" +#import "ECOSocketChannel.h" + +typedef void(^ECOChannelReceivedPacket)(ECOChannelDeviceInfo *device, NSData *data, NSDictionary *extraInfo); +typedef void(^ECOChannelDeviceConnectedStatusChanged)(ECOChannelDeviceInfo *device, BOOL isConnected); + +typedef void(^ECOChannelAuthStateChangedBlock)(ECOChannelDeviceInfo *device, ECOAuthorizeResponseType authState); +typedef void(^ECOChannelRequestAuthStateBlock)(ECOChannelDeviceInfo *device, ECOAuthorizeResponseType authState); + +@interface ECOChannelManager : NSObject + +/** + 接收到数据回调 + */ +@property (nonatomic, copy) ECOChannelReceivedPacket receivedBlock; + +/** + 设备连接变更 + */ +@property (nonatomic, copy) ECOChannelDeviceConnectedStatusChanged deviceBlock; + +/// 授权状态变更回调 +@property (nonatomic, copy) ECOChannelAuthStateChangedBlock authStateChangedBlock; + +/// 请求授权状态认证回调 +@property (nonatomic, copy) ECOChannelRequestAuthStateBlock requestAuthBlock; + +/// 设备白名单列表,记录始终允许的设备 +@property (nonatomic, strong, readonly) NSMutableArray *whitelistDevices; + +/** + 发送数据包 + + @param packet 数据包 + @param type 数据包类型,json或者普通数据包 + @param extraInfo 透传信息 + @param device 要接收消息的设备,如果传入nil,则对所有已授权连接的设备发送消息 + */ +- (void)sendPacket:(NSData *)packet +// type:(ECOPacketDataType)type + extraInfo:(NSDictionary *)extraInfo + toDevice:(ECOChannelDeviceInfo *)device; + +//发送授权数据 +- (void)sendAuthorizationMessageToDevice:(ECOChannelDeviceInfo *)device + state:(ECOAuthorizeResponseType)responseType + showAuthAlert:(BOOL)showAuthAlert; + +/** + 是否已连接到Mac客户端 + + @return 连接状态 + */ +- (BOOL)isConnected; + +//链接IP地址的主机 +- (void)connectToClientIPAddress:(NSString *)ipAddress; + +@end diff --git a/Src/Main/Shared/Channel/ECOChannelManager.m b/Src/Main/Shared/Channel/ECOChannelManager.m new file mode 100644 index 00000000..d6ac3eff --- /dev/null +++ b/Src/Main/Shared/Channel/ECOChannelManager.m @@ -0,0 +1,116 @@ +// +// ECOChannelManager.m +// Echo +// +// Created by 陈爱彬 on 2019/4/16. Maintain by 陈爱彬 +// Description +// + +#import "ECOChannelManager.h" + +@interface ECOChannelManager() + + +@property (nonatomic, strong) ECOUSBChannel *ptChannel; +@property (nonatomic, strong) ECOSocketChannel *socketChannel; + +@end + +@implementation ECOChannelManager + +- (instancetype)init +{ + self = [super init]; + if (self) { + _socketChannel = [[ECOSocketChannel alloc] initWithDelegate:self]; + _ptChannel = [[ECOUSBChannel alloc] initWithDelegate:self]; +#if TARGET_OS_IPHONE + __weak typeof(self) weakSelf = self; + _ptChannel.attachBlock = ^(NSString *ipAddress) { + [weakSelf.socketChannel connectToIPAddress:ipAddress]; + }; +#endif + + } + return self; +} + +#pragma mark - 优先级管理 + +#pragma mark - 通道连接 +- (void)connectToClientIPAddress:(NSString *)ipAddress { + [self.socketChannel autoConnectToClientIPAddress:ipAddress]; +} +#pragma mark - 数据传输 +//发送数据 +- (void)sendPacket:(NSData *)packet +// type:(ECOPacketDataType)type + extraInfo:(NSDictionary *)extraInfo + toDevice:(ECOChannelDeviceInfo *)device { + if (!packet || + ![packet isKindOfClass:[NSData class]]) { + return; + } + [self.socketChannel sendPacket:packet extraInfo:extraInfo toDevice:device]; +} +//接收数据 +- (void)device:(ECOChannelDeviceInfo *)device receivePacket:(NSData *)packet extraInfo:(NSDictionary *)extraInfo { + if (!packet) { + return; + } + !self.receivedBlock ?: self.receivedBlock(device, packet, extraInfo); +} +//发送授权数据 +- (void)sendAuthorizationMessageToDevice:(ECOChannelDeviceInfo *)device + state:(ECOAuthorizeResponseType)responseType + showAuthAlert:(BOOL)showAuthAlert { + if (!device || + ![device isKindOfClass:[ECOChannelDeviceInfo class]]) { + return; + } + [self.socketChannel sendAuthorizationMessageToDevice:device state:responseType showAuthAlert:showAuthAlert]; +} +#pragma mark - 连接状态 +// 是否已连接到Mac客户端 +- (BOOL)isConnected { + BOOL isSocketConnected = [_socketChannel isConnected]; + return isSocketConnected; +} +#pragma mark - ECOChannelConnectedDeviceProtocol methods +- (void)channel:(ECOBaseChannel *)channel didConnectedToDevice:(ECOChannelDeviceInfo *)device { + NSLog(@">> [ECOChannelManager] did Connected to device:%@", [device description]); +// //状态回调 +// if (device.authorizedType != ECOAuthorizeResponseType_Deny) { +// !self.connectBlock ?: self.connectBlock([self isConnected]); +// } + //连接设备状态 + !self.deviceBlock ?: self.deviceBlock(device, YES); +} +- (void)channel:(ECOBaseChannel *)channel didDisconnectWithDevice:(ECOChannelDeviceInfo *)device { + NSLog(@">> [ECOChannelManager] did Disconnect to device:%@", [device description]); +// //状态回调 +// if (device.authorizedType != ECOAuthorizeResponseType_Deny) { +// !self.connectBlock ?: self.connectBlock([self isConnected]); +// } + //连接设备状态 + !self.deviceBlock ?: self.deviceBlock(device, NO); +} +- (void)channel:(ECOBaseChannel *)channel didReceivedDevice:(ECOChannelDeviceInfo *)device andData:(NSData *)data extraInfo:(NSDictionary *)extraInfo{ +// NSLog(@">> [ECOChannelManager] did Received data from device:%@", [device description]); + [self device:device receivePacket:data extraInfo:extraInfo]; +} + +- (void)channel:(ECOBaseChannel *)channel device:(ECOChannelDeviceInfo *)device didChangedAuthState:(ECOAuthorizeResponseType)authorizedType { + NSLog(@">> [ECOChannelManager] device did changed authState:%@", @(authorizedType)); + !self.authStateChangedBlock ?: self.authStateChangedBlock(device, authorizedType); +} +- (void)channel:(ECOBaseChannel *)channel device:(ECOChannelDeviceInfo *)device willRequestAuthState:(ECOAuthorizeResponseType)authorizedType { + NSLog(@">> [ECOChannelManager] device will request authState:%@", @(authorizedType)); + !self.requestAuthBlock ?: self.requestAuthBlock(device, authorizedType); +} + +- (NSMutableArray *)whitelistDevices { + return self.socketChannel.whitelistDevices; +} + +@end diff --git a/Src/Main/Shared/Channel/ECOSocketChannel.h b/Src/Main/Shared/Channel/ECOSocketChannel.h new file mode 100644 index 00000000..6f12b74c --- /dev/null +++ b/Src/Main/Shared/Channel/ECOSocketChannel.h @@ -0,0 +1,60 @@ +// +// ECOSocketChannel.h +// Echo +// +// Created by 陈爱彬 on 2019/4/16. Maintain by 陈爱彬 +// Description +// + +#import "ECOBaseChannel.h" +//#import "ECOCoreDefines.h" + +typedef NS_ENUM(NSInteger, ECOSocketHeadTag) { + ECOSocketHeadTag_Data = 0, //普通数据 + ECOSocketHeadTag_Device = 1, //设备信息 + ECOSocketHeadTag_Authorization = 2, //授权连接 + ECOSocketHeadTag_ClientListen = 3, //Client监听Mac端的主动申请 +}; + +typedef NS_ENUM(NSInteger, ECOSocketDataTag) { + ECOSocketDataTag_Data = 10, //普通数据 + ECOSocketDataTag_Device = 11, //设备信息 + ECOSocketDataTag_Authorization = 12, //授权连接 + ECOSocketDataTag_ClientListen = 13, //Client监听Mac端的主动申请 +}; + +//版本号,版本不一致忽略该数据 +static const int ECOSocketProtocolVersion = 1; + +//包体主协议,1表示授权连接,2表示发送数据,3表示Client侧接收主动申请 +static const int ECOHeadMainType_Authorization = 1; +static const int ECOHeadMainType_Data = 2; +static const int ECOHeadMainType_ClientListen = 3; + +//包体子协议,0表示JSON数据,1表示普通NSData数据 +static const int ECOHeadSubType_JSON = 0; +static const int ECOHeadSubType_Data = 1; + +@interface ECOSocketChannel : ECOBaseChannel + +/// 设备白名单列表,记录始终允许的设备 +@property (nonatomic, strong) NSMutableArray *whitelistDevices; + +//连接到ip地址 +- (void)autoConnectToClientIPAddress:(NSString *)ipAddress; + +//Client侧连接Mac侧:连接到ip地址 +- (void)connectToIPAddress:(NSString *)ip; + +//发送数据 +- (void)sendPacket:(NSData *)packet +// type:(ECOPacketDataType)type + extraInfo:(NSDictionary *)extraInfo + toDevice:(ECOChannelDeviceInfo *)device; + +//发送授权数据 +- (void)sendAuthorizationMessageToDevice:(ECOChannelDeviceInfo *)device + state:(ECOAuthorizeResponseType)responseType + showAuthAlert:(BOOL)showAuthAlert; + +@end diff --git a/Src/Main/Shared/Channel/ECOSocketChannel.m b/Src/Main/Shared/Channel/ECOSocketChannel.m new file mode 100644 index 00000000..da957ec5 --- /dev/null +++ b/Src/Main/Shared/Channel/ECOSocketChannel.m @@ -0,0 +1,546 @@ +// +// ECOSocketChannel.m +// Echo +// +// Created by 陈爱彬 on 2019/4/16. Maintain by 陈爱彬 +// Description +// + +#import "ECOSocketChannel.h" +#import +#import +#include + +#import "ECONetServicePublisher.h" +#import "ECONetServiceBrowser.h" + +#import "LookinDefines.h" + +//static uint16_t const ECOClientSockeListenPortNumber = 23235; +//static uint16_t const ECOSocketAcceptPortNumber = 23234; +static CGFloat const ECOSocketRetryListenDelay = 1.f; +static NSInteger const ECOSocketHeadOffsetValue = 10; +static NSString *const ECHOAuthorizedDevicesKey = @"echoAuthorizedDevicesKey"; + +@interface ECOSocketChannel() + { + NSRecursiveLock *_socketLock; +} +@property (nonatomic, strong) NSMutableArray *sockets; +@property (nonatomic, strong) GCDAsyncSocket *mSocket; +@property (nonatomic, strong) ECONetServicePublisher *publisher; +@property (nonatomic, strong) ECONetServiceBrowser *browser; +//Client端主动连接的监听socket +@property (nonatomic, strong) GCDAsyncSocket *cSocket; +@property (nonatomic, strong) NSMutableArray *clientSockets; + +@end + +@implementation ECOSocketChannel + +- (void)dealloc { + NSLog(@"%s",__func__); +} +//初始化 +- (void)setupChannel { + [super setupChannel]; + _socketLock = [[NSRecursiveLock alloc] init]; + self.priority = ECOChannelPriority_Socket; + + //开启服务监听 +#if TARGET_OS_OSX + [self startListening]; +#else + [self setupClientListenSocket]; + [self.browser startBrowsing]; +#endif +} +//是否有socket连接 +- (BOOL)isConnected { + BOOL v = NO; + [self p_lock]; + if (self.connectType == ECOChannelConnectType_Authorization) { + for (GCDAsyncSocket *socket in self.sockets) { + ECOChannelDeviceInfo *deviceInfo = socket.userData; + if (deviceInfo.authorizedType != ECOAuthorizeResponseType_Deny) { + v = YES; + break; + } + } + }else{ + NSInteger count = [self.sockets count]; + v = count != 0; + } + [self p_unlock]; + return v; +} +//判断某个设备是否已连接 +- (BOOL)isEchoConnectedOfDevice:(ECOChannelDeviceInfo *)device { + BOOL v = NO; + [self p_lock]; + for (GCDAsyncSocket *socket in self.sockets) { + ECOChannelDeviceInfo *deviceInfo = socket.userData; + if ([device.ipAddress isEqualToString:deviceInfo.ipAddress]) { + v = YES; + break; + } + } + [self p_unlock]; + return v; +} +- (void)setupClientListenSocket { + self.cSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)]; + NSError *error = nil; + [self.cSocket acceptOnPort:LookinClientSockeListenPortNumber error:&error]; + if (error) { + NSLog(@"Socket Listen error:%@", error); + self.cSocket.delegate = nil; + self.cSocket = nil; + } +} +- (void)startListening { + [self p_lock]; + [self.sockets removeAllObjects]; + [self p_unlock]; + /* + https://developer.apple.com/library/archive/documentation/Networking/Conceptual/NSNetServiceProgGuide/Articles/PublishingServices.html + 创建listening socket for communication + */ + self.mSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)]; + NSError *error = nil; + BOOL isAccept = [self.mSocket acceptOnPort:LookinSocketAcceptPortNumber error:&error]; + if (error) { + NSLog(@"Socket Listen error:%@", error); + self.mSocket.delegate = nil; + self.mSocket = nil; + [self restartListening]; + return; + } + NSLog(@"Socket Listen:%@", isAccept ? @"Success" : @"Failure"); + //启动NetService + [self.publisher startPublish]; +} + +//重试监听 +- (void)restartListening { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(ECOSocketRetryListenDelay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self startListening]; + }); +} +#pragma mark - Socket连接 +//Client侧连接Mac侧 +- (void)connectToAddresses:(NSArray *)addresses + hostName:(NSString *)hostName { + if (!addresses || ![addresses count]) { + return; + } + GCDAsyncSocket *socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)]; + NSData *address = [addresses objectAtIndex:0]; + NSError *error = nil; + BOOL connected = [socket connectToAddress:address error:&error]; + if (connected) { + [self p_lock]; + [self.sockets addObject:socket]; + [self p_unlock]; + //发送设备信息 + [self sendDeviceInfo:socket hostName:hostName]; + } + if (error) { + NSLog(@">> [ECOSocketChannel] connect to address failed:%@", error); + } +} +//Client侧连接Mac侧:连接到ip地址 +- (void)connectToIPAddress:(NSString *)ip { + if (!ip || ![ip length]) { + return; + } + GCDAsyncSocket *socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)]; + NSError *error = nil; + BOOL connected = [socket connectToHost:ip onPort:LookinSocketAcceptPortNumber error:&error]; + if (connected) { + [self p_lock]; + [self.sockets addObject:socket]; + [self p_unlock]; + //发送设备信息 + [self sendDeviceInfo:socket hostName:nil]; + } + if (error) { + NSLog(@">> [ECOSocketChannel] connect to address failed:%@", error); + } +} +//Mac侧连接Client侧 +- (void)autoConnectToClientIPAddress:(NSString *)ipAddress { + if (!ipAddress || ![ipAddress length]) { + return; + } + GCDAsyncSocket *socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)]; + NSError *error = nil; + BOOL connected = [socket connectToHost:ipAddress onPort:LookinClientSockeListenPortNumber error:&error]; + if (connected) { + [self.clientSockets addObject:socket]; + //发送本Mac侧的信息 + [self sendMacAutoConnectInfoInfo:socket]; + } + if (error) { + NSLog(@">> [ECOSocketChannel] autoConnectToClientIPAddress failed:%@", error); + } +} +#pragma mark - Socket通信 +//发送数据 +- (void)sendPacket:(NSData *)packet +// type:(ECOPacketDataType)type + extraInfo:(NSDictionary *)extraInfo + toDevice:(ECOChannelDeviceInfo *)device { + if (!packet) { + return; + } + + NSMutableData *buffer = [[NSMutableData alloc] init]; + NSMutableDictionary *header = [NSMutableDictionary dictionary]; + [header setValue:@(ECOSocketProtocolVersion) forKey:@"version"]; + [header setValue:@(ECOHeadMainType_Data) forKey:@"mType"]; +// [header setValue:@(type) forKey:@"sType"]; + [header setValue:@([packet length]) forKey:@"len"]; +// if (type == ECOPacketDataType_Data) { + [header setValue:extraInfo forKey:@"extra"]; +// } + NSData *headData = [NSJSONSerialization dataWithJSONObject:header options:0 error:nil]; + [buffer appendData:headData]; + [buffer appendData:[GCDAsyncSocket CRLFData]]; + [buffer appendBytes:[packet bytes] length:[packet length]]; + + [self p_lock]; + for (GCDAsyncSocket *socket in self.sockets) { + if (self.connectType == ECOChannelConnectType_Authorization) { + //授权机制 + ECOChannelDeviceInfo *deviceInfo = socket.userData; + if (deviceInfo.authorizedType != ECOAuthorizeResponseType_Deny) { + if (device != nil) { + //给单个设备发送消息 + if ([device isEqual:deviceInfo]) { + [socket writeData:buffer withTimeout:-1 tag:ECOSocketHeadTag_Data]; + } + }else{ + //给所有设备发送消息 + [socket writeData:buffer withTimeout:-1 tag:ECOSocketHeadTag_Data]; + } + } + }else{ + [socket writeData:buffer withTimeout:-1 tag:ECOSocketHeadTag_Data]; + } + } + [self p_unlock]; +} +//发送授权数据 +- (void)sendAuthorizationMessageToDevice:(ECOChannelDeviceInfo *)device + state:(ECOAuthorizeResponseType)responseType + showAuthAlert:(BOOL)showAuthAlert { + //修改数据 + ECOChannelDeviceInfo *authDevice = device; + device.showAuthAlert = showAuthAlert; +#if TARGET_OS_OSX + if (responseType != ECOAuthorizeResponseType_Deny) { + //允许需要等待对方确认,稍后修改device的内容 + authDevice = [device copy]; + } +#endif + authDevice.authorizedType = responseType; + NSError *error = nil; + NSData *packet = [NSJSONSerialization dataWithJSONObject:[authDevice toJSONObject] options:0 error:&error]; + if (error) { + NSLog(@">>[ECOCoreManager] send AuthorizationResponse failed:%@", error); + } + [self p_lock]; + for (GCDAsyncSocket *socket in self.sockets) { + ECOChannelDeviceInfo *deviceInfo = socket.userData; + if ([deviceInfo isEqual:authDevice]) { + NSMutableData *buffer = [[NSMutableData alloc] init]; + NSMutableDictionary *header = [NSMutableDictionary dictionary]; + [header setValue:@(ECOSocketProtocolVersion) forKey:@"version"]; + [header setValue:@(ECOHeadMainType_Authorization) forKey:@"mType"]; + [header setValue:@(ECOHeadSubType_JSON) forKey:@"sType"]; + [header setValue:@([packet length]) forKey:@"len"]; + NSData *headData = [NSJSONSerialization dataWithJSONObject:header options:0 error:nil]; + [buffer appendData:headData]; + [buffer appendData:[GCDAsyncSocket CRLFData]]; + [buffer appendBytes:[packet bytes] length:[packet length]]; + [socket writeData:buffer withTimeout:-1 tag:ECOSocketHeadTag_Data]; + } + } + [self p_unlock]; + +#if TARGET_OS_OSX + //授权连接回调,手动断开时主动回调,主动连接时等收到对方的消息再回调 + if (responseType == ECOAuthorizeResponseType_Deny) { + if ([self.delegate respondsToSelector:@selector(channel:device:didChangedAuthState:)]) { + [self.delegate channel:self device:device didChangedAuthState:NO]; + } + //修改授权白名单 + if (device.uuid.length > 0) { + NSString *uniId = [NSString stringWithFormat:@"%@_%@",device.uuid, device.appInfo.appId]; + if ([self.whitelistDevices containsObject:uniId]) { + [self.whitelistDevices removeObject:uniId]; + [self saveWhiteListDevices]; + } + } + } +#else + if ([self.delegate respondsToSelector:@selector(channel:device:didChangedAuthState:)]) { + [self.delegate channel:self device:device didChangedAuthState:responseType]; + } +#endif + +} +//发送设备信息 +- (void)sendDeviceInfo:(GCDAsyncSocket *)socket + hostName:(NSString *)hostName { + ECOChannelDeviceInfo *deviceInfo = [ECOChannelDeviceInfo new]; + deviceInfo.hostName = hostName; + NSData *packet = [NSJSONSerialization dataWithJSONObject:[deviceInfo toJSONObject] options:0 error:nil]; + if (!packet) { + return; + } + NSMutableData *buffer = [[NSMutableData alloc] init]; + NSMutableDictionary *header = [NSMutableDictionary dictionary]; + [header setValue:@(ECOSocketProtocolVersion) forKey:@"version"]; + [header setValue:@(ECOHeadMainType_Data) forKey:@"mType"]; + [header setValue:@(ECOHeadSubType_JSON) forKey:@"sType"]; + [header setValue:@([packet length]) forKey:@"len"]; + NSData *headData = [NSJSONSerialization dataWithJSONObject:header options:0 error:nil]; + [buffer appendData:headData]; + [buffer appendData:[GCDAsyncSocket CRLFData]]; + [buffer appendBytes:[packet bytes] length:[packet length]]; + [socket writeData:buffer withTimeout:-1 tag:ECOSocketHeadTag_Device]; +} +//发送给Client侧消息 +- (void)sendMacAutoConnectInfoInfo:(GCDAsyncSocket *)socket { + ECOChannelDeviceInfo *deviceInfo = [ECOChannelDeviceInfo new]; + NSData *packet = [NSJSONSerialization dataWithJSONObject:[deviceInfo toJSONObject] options:0 error:nil]; + if (!packet) { + return; + } + NSMutableData *buffer = [[NSMutableData alloc] init]; + NSMutableDictionary *header = [NSMutableDictionary dictionary]; + [header setValue:@(ECOSocketProtocolVersion) forKey:@"version"]; + [header setValue:@(ECOHeadMainType_ClientListen) forKey:@"mType"]; + [header setValue:@(ECOHeadSubType_JSON) forKey:@"sType"]; + [header setValue:@([packet length]) forKey:@"len"]; + NSData *headData = [NSJSONSerialization dataWithJSONObject:header options:0 error:nil]; + [buffer appendData:headData]; + [buffer appendData:[GCDAsyncSocket CRLFData]]; + [buffer appendBytes:[packet bytes] length:[packet length]]; + [socket writeData:buffer withTimeout:-1 tag:ECOSocketHeadTag_ClientListen]; +} +#pragma mark - GCDAsyncSocketDelegate methods +- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port { + // NSLog(@"%s",__func__); +#if TARGET_OS_IPHONE + //读取数据 + [sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:ECOSocketHeadTag_Device]; +#endif +} + +- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket { + // NSLog(@"%s",__func__); +#if TARGET_OS_OSX + [self p_lock]; + [self.sockets addObject:newSocket]; + newSocket.delegate = self; + [newSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:ECOSocketHeadTag_Device]; + [self p_unlock]; + //发送设备信息 + [self sendDeviceInfo:newSocket hostName:nil]; +#else + [self.clientSockets addObject:newSocket]; + newSocket.delegate = self; + [newSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:ECOSocketHeadTag_ClientListen]; +#endif +} + +- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { +// NSLog(@"%s",__func__); + if (tag == ECOSocketHeadTag_Data || + tag == ECOSocketHeadTag_Device || + tag == ECOSocketHeadTag_ClientListen) { + NSData *headerData = [data subdataWithRange:NSMakeRange(0, data.length - [GCDAsyncSocket CRLFData].length)]; + NSDictionary *header = [NSJSONSerialization JSONObjectWithData:headerData options:NSJSONReadingAllowFragments error:nil]; + NSInteger version = [header[@"version"] integerValue]; + if (version < ECOSocketProtocolVersion) { + return; + } + NSInteger length = [header[@"len"] integerValue]; + NSInteger mainType = [header[@"mType"] integerValue]; + if (mainType == ECOHeadMainType_Authorization) { + [sock readDataToLength:length withTimeout:-1 tag:ECOSocketDataTag_Authorization]; + }else if(mainType == ECOHeadMainType_Data) { + ECOChannelDeviceInfo *deviceInfo = sock.userData; + NSDictionary *extraInfo = header[@"extra"]; + deviceInfo.extraData = extraInfo; + [sock readDataToLength:length withTimeout:-1 tag:tag + ECOSocketHeadOffsetValue]; + }else if (mainType == ECOHeadMainType_ClientListen) { + [sock readDataToLength:length withTimeout:-1 tag:tag + ECOSocketHeadOffsetValue]; + } + return; + } + if (tag == ECOSocketDataTag_Device) { + ECOChannelDeviceInfo *deviceInfo = [[ECOChannelDeviceInfo alloc] initWithData:data]; + sock.userData = deviceInfo; +#if TARGET_OS_OSX + NSString *uniId = [NSString stringWithFormat:@"%@_%@",deviceInfo.uuid, deviceInfo.appInfo.appId]; + if ([self.whitelistDevices containsObject:uniId]) { + //设置为已授权 + deviceInfo.authorizedType = ECOAuthorizeResponseType_AllowAlways; + [self sendAuthorizationMessageToDevice:deviceInfo state:ECOAuthorizeResponseType_AllowAlways showAuthAlert:NO]; + } +#endif + deviceInfo.isConnected = YES; + //连接到新设备 + if ([self.delegate respondsToSelector:@selector(channel:didConnectedToDevice:)]) { + [self.delegate channel:self didConnectedToDevice:deviceInfo]; + } + } + if (tag == ECOSocketDataTag_ClientListen) { + ECOChannelDeviceInfo *deviceInfo = [[ECOChannelDeviceInfo alloc] initWithData:data]; + BOOL isConnected = [self isEchoConnectedOfDevice:deviceInfo]; + if (!isConnected) { + [self connectToIPAddress:deviceInfo.ipAddress]; + }else{ + NSLog(@"当前Echo主机已连接:%@",deviceInfo.ipAddress); + } + [sock setDelegate:nil]; + [self.clientSockets removeObject:sock]; + } + if (tag == ECOSocketDataTag_Authorization) { + //授权信息 + ECOChannelDeviceInfo *deviceInfo = sock.userData; + ECOChannelDeviceInfo *tempDevice = [[ECOChannelDeviceInfo alloc] initWithData:data]; + BOOL isAlwaysAllow = tempDevice.authorizedType == ECOAuthorizeResponseType_AllowAlways; +#if TARGET_OS_IPHONE + deviceInfo.hostName = tempDevice.hostName; + if (tempDevice.showAuthAlert) { + //弹窗提示用户 + if ([self.delegate respondsToSelector:@selector(channel:device:willRequestAuthState:)]) { + [self.delegate channel:self device:deviceInfo willRequestAuthState:tempDevice.authorizedType]; + } + }else{ + deviceInfo.authorizedType = tempDevice.authorizedType; + //连接到新设备 + if ([self.delegate respondsToSelector:@selector(channel:device:didChangedAuthState:)]) { + [self.delegate channel:self device:deviceInfo didChangedAuthState:tempDevice.authorizedType]; + } + } +#endif +#if TARGET_OS_OSX + //修改设备的授权状态 + deviceInfo.authorizedType = tempDevice.authorizedType; + //重置标记位 + deviceInfo.showAuthAlert = NO; + //始终允许,加入白名单 + if (deviceInfo.uuid.length > 0) { + NSString *uniId = [NSString stringWithFormat:@"%@_%@",deviceInfo.uuid, deviceInfo.appInfo.appId]; + if (isAlwaysAllow && ![self.whitelistDevices containsObject:uniId]) { + [self.whitelistDevices addObject:uniId]; + [self saveWhiteListDevices]; + }else if (!isAlwaysAllow && [self.whitelistDevices containsObject:uniId]){ + [self.whitelistDevices removeObject:uniId]; + [self saveWhiteListDevices]; + } + } + //回调 + if ([self.delegate respondsToSelector:@selector(channel:device:didChangedAuthState:)]) { + [self.delegate channel:self device:deviceInfo didChangedAuthState:tempDevice.authorizedType]; + } +#endif + } + //传递数据给上层 + if (tag == ECOSocketDataTag_Data) { + if ([self.delegate respondsToSelector:@selector(channel:didReceivedDevice:andData:extraInfo:)]) { + ECOChannelDeviceInfo *deviceInfo = sock.userData; + if (deviceInfo.authorizedType != ECOAuthorizeResponseType_Deny) { + //未授权的通信忽略 + [self.delegate channel:self didReceivedDevice:deviceInfo andData:data extraInfo:deviceInfo.extraData]; + } + } + } + //读取下次发送的数据 + [sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:ECOSocketHeadTag_Data]; +} +- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err { +// NSLog(@"%s",__func__); + [self p_lock]; + [sock setDelegate:nil]; + if ([self.sockets containsObject:sock]) { + if ([self.delegate respondsToSelector:@selector(channel:didDisconnectWithDevice:)]) { + ECOChannelDeviceInfo *deviceInfo = sock.userData; + deviceInfo.isConnected = NO; + [self.delegate channel:self didDisconnectWithDevice:deviceInfo]; + } + [self.sockets removeObject:sock]; + } + if ([self.clientSockets containsObject:sock]) { + [self.clientSockets removeObject:sock]; + } + [self p_unlock]; + //重启监听 + if (sock == self.mSocket) { + [self restartListening]; + } +} +#pragma mark - helper +- (void)p_lock { + [_socketLock lock]; +} +- (void)p_unlock { + [_socketLock unlock]; +} +- (void)saveWhiteListDevices { + NSArray *list = [self.whitelistDevices copy]; + [[NSUserDefaults standardUserDefaults] setObject:list forKey:ECHOAuthorizedDevicesKey]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} +#pragma mark - getters +- (NSMutableArray *)sockets { + if (!_sockets) { + _sockets = [[NSMutableArray alloc] initWithCapacity:0]; + } + return _sockets; +} +- (NSMutableArray *)clientSockets { + if (!_clientSockets) { + _clientSockets = [[NSMutableArray alloc] initWithCapacity:0]; + } + return _clientSockets; +} + +#if TARGET_OS_OSX +- (ECONetServicePublisher *)publisher { + if (!_publisher) { + _publisher = [[ECONetServicePublisher alloc] init]; + } + return _publisher; +} +#endif + + +#if TARGET_OS_IPHONE +- (ECONetServiceBrowser *)browser { + if (!_browser) { + _browser = [[ECONetServiceBrowser alloc] init]; + __weak typeof(self) weakSelf = self; + _browser.addressesBlock = ^(NSArray *addresses, NSString *hostName) { + __strong typeof(weakSelf) strongSelf = weakSelf; + [strongSelf connectToAddresses:addresses hostName:hostName]; + }; + } + return _browser; +} +#endif + +- (NSMutableArray *)whitelistDevices { + if (!_whitelistDevices) { + NSArray *list = [[NSUserDefaults standardUserDefaults] objectForKey:ECHOAuthorizedDevicesKey]; + _whitelistDevices = [NSMutableArray arrayWithArray:list ?: @[]]; + } + return _whitelistDevices; +} +@end diff --git a/Src/Main/Shared/Channel/ECOUSBChannel.h b/Src/Main/Shared/Channel/ECOUSBChannel.h new file mode 100644 index 00000000..15e72462 --- /dev/null +++ b/Src/Main/Shared/Channel/ECOUSBChannel.h @@ -0,0 +1,29 @@ +// +// ECOUSBChannel.h +// Echo +// +// Created by 陈爱彬 on 2019/4/16. Maintain by 陈爱彬 +// Description +// + +#import "ECOBaseChannel.h" + +typedef NS_ENUM(NSInteger, ECOUSBChannelType) { + ECOUSBChannelType_Command = 100, + ECOUSBChannelType_TextMessage = 101, + ECOUSBChannelType_Ping = 102, + ECOUSBChannelType_Pong = 103, +}; + +typedef struct _ECOUSBChannelTextFrame { + uint32_t length; + uint8_t utf8text[0]; +} ECOUSBChannelTextFrame; + +typedef void(^ECOUSBChannelDidAttachBlock)(NSString *ipAddress); + +@interface ECOUSBChannel : ECOBaseChannel + +@property (nonatomic, copy) ECOUSBChannelDidAttachBlock attachBlock; + +@end diff --git a/Src/Main/Shared/Channel/ECOUSBChannel.m b/Src/Main/Shared/Channel/ECOUSBChannel.m new file mode 100644 index 00000000..a1d17252 --- /dev/null +++ b/Src/Main/Shared/Channel/ECOUSBChannel.m @@ -0,0 +1,325 @@ +// +// ECOUSBChannel.m +// Echo +// +// Created by 陈爱彬 on 2019/4/16. Maintain by 陈爱彬 +// Description +// + +#import "ECOUSBChannel.h" +#import "Lookin_PTChannel.h" +#import "ECOChannelDeviceInfo.h" + +#if TARGET_OS_OSX +#import +#endif + +static const int ECOUSBChannelIPv4PortNumber = 2333; +static const NSTimeInterval ECOUSBChannelReconnectDelay = 1.0; + +@interface ECOUSBChannel() + { + dispatch_queue_t _notConnectedQueue; +} + +@property (nonatomic, weak) Lookin_PTChannel *serverChannel; +@property (nonatomic, weak) Lookin_PTChannel *peerChannel; + +@property (nonatomic, strong) NSNumber *connectingToDeviceID; +@property (nonatomic, strong) NSNumber *connectedDeviceID; +@property (nonatomic, strong) NSDictionary *connectedDeviceProperties; +@property (nonatomic, strong) Lookin_PTChannel *connectedChannel; + +@end + +@implementation ECOUSBChannel + +#pragma mark - LifeCycle methods +- (void)dealloc { + NSLog(@"%s",__func__); + if (self.serverChannel) { + [self.serverChannel close]; + } + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} +//初始化 +- (void)setupChannel { + [super setupChannel]; + self.priority = ECOChannelPriority_USB; + +#if TARGET_OS_OSX + _notConnectedQueue = dispatch_queue_create("com.echo.notConnectedQueue", DISPATCH_QUEUE_SERIAL); + // Start listening for device attached/detached notifications + [self startListeningForDevices]; + // Start trying to connect to local IPv4 port (defined in PTExampleProtocol.h) + [self enqueueConnectToLocalIPv4Port]; + + [self ping]; +#else + [self setupPTChannel]; +#endif +} +//是否有usb连接 +- (BOOL)isConnected { +#if TARGET_OS_OSX + return self.connectedDeviceID != nil; +#else + return self.peerChannel != nil; +#endif +} +//创建channel +- (void)setupPTChannel { + Lookin_PTChannel *channel = [Lookin_PTChannel channelWithDelegate:self]; + [channel listenOnPort:ECOUSBChannelIPv4PortNumber IPv4Address:INADDR_LOOPBACK callback:^(NSError *error) { + if (error) { + NSLog(@">> [ECOUSBChannel] Failed to listen on 127.0.0.1:%d: %@", ECOUSBChannelIPv4PortNumber, error); + }else{ + NSLog(@">> [ECOUSBChannel] Listening on 127.0.0.1:%d", ECOUSBChannelIPv4PortNumber); + self.serverChannel = channel; + } + }]; +} + +#pragma mark - Wired device connections +- (void)startListeningForDevices { + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center addObserver:self selector:@selector(onUSBDeviceDidAttach:) name:Lookin_PTUSBDeviceDidAttachNotification object:Lookin_PTUSBHub.sharedHub]; + [center addObserver:self selector:@selector(onUSBDeviceDidDetach:) name:Lookin_PTUSBDeviceDidDetachNotification object:Lookin_PTUSBHub.sharedHub]; +} +- (void)onUSBDeviceDidAttach:(NSNotification *)note { + NSNumber *deviceID = [note.userInfo objectForKey:@"DeviceID"]; + NSLog(@"<< [ECOUSBChannel] PTUSBDeviceDidAttachNotification:%@", deviceID); + // [self showAlertWithMessage:[NSString stringWithFormat:@"usb设备连接:%@",deviceID]]; + + dispatch_async(_notConnectedQueue, ^{ + if (!self.connectingToDeviceID || + ![deviceID isEqualToNumber:self.connectingToDeviceID]) { + [self disconnectFromCurrentChannel]; + self.connectingToDeviceID = deviceID; + self.connectedDeviceProperties = [note.userInfo objectForKey:@"Properties"]; + [self enqueueConnectToUSBDevice]; + } + }); +} +- (void)onUSBDeviceDidDetach:(NSNotification *)note { + NSNumber *deviceID = [note.userInfo objectForKey:@"DeviceID"]; + NSLog(@"<< [ECOUSBChannel] PTUSBDeviceDidDetachNotification:%@", deviceID); + // [self showAlertWithMessage:[NSString stringWithFormat:@"usb设备断开:%@",deviceID]]; + + if ([self.connectingToDeviceID isEqualToNumber:deviceID]) { + self.connectedDeviceProperties = nil; + self.connectingToDeviceID = nil; + if (self.connectedChannel) { + [self.connectedChannel close]; + } + } +} + +- (void)enqueueConnectToLocalIPv4Port { + dispatch_async(_notConnectedQueue, ^{ + dispatch_async(dispatch_get_main_queue(), ^{ + [self connectToLocalIPv4Port]; + }); + }); +} + +- (void)connectToLocalIPv4Port { + Lookin_PTChannel *channel = [Lookin_PTChannel channelWithDelegate:self]; + channel.userInfo = [NSString stringWithFormat:@"127.0.0.1:%d", ECOUSBChannelIPv4PortNumber]; + [channel connectToPort:ECOUSBChannelIPv4PortNumber IPv4Address:INADDR_LOOPBACK callback:^(NSError *error, Lookin_PTAddress *address) { + if (error) { + if (error.domain == NSPOSIXErrorDomain && (error.code == ECONNREFUSED || error.code == ETIMEDOUT)) { + // this is an expected state + }else{ + NSLog(@"<< [ECOUSBChannel] Failed to connect to 127.0.0.1:%d: %@", ECOUSBChannelIPv4PortNumber, error); + } + [self performSelector:@selector(enqueueConnectToLocalIPv4Port) withObject:nil afterDelay:ECOUSBChannelReconnectDelay]; + }else{ + [self disconnectFromCurrentChannel]; + self.connectedChannel = channel; + channel.userInfo = address; + NSLog(@"<< [ECOUSBChannel] Connected to %@", address); + } + }]; +} + +- (void)enqueueConnectToUSBDevice { + dispatch_async(_notConnectedQueue, ^{ + dispatch_async(dispatch_get_main_queue(), ^{ + [self connectToUSBDevice]; + }); + }); +} + +- (void)connectToUSBDevice { + Lookin_PTChannel *channel = [Lookin_PTChannel channelWithDelegate:self]; + channel.userInfo = self.connectingToDeviceID; + channel.delegate = self; + [channel connectToPort:ECOUSBChannelIPv4PortNumber overUSBHub:Lookin_PTUSBHub.sharedHub deviceID:self.connectingToDeviceID callback:^(NSError *error) { + if (error) { + if (error.domain == Lookin_PTUSBHubErrorDomain && error.code == PTUSBHubErrorConnectionRefused) { + // NSLog(@"<< [ECOUSBChannel] Failed to connect to device #%@: %@", channel.userInfo, error); + }else{ + // NSLog(@"<< [ECOUSBChannel] Failed to connect to device #%@: %@", channel.userInfo, error); + } + if (channel.userInfo == self.connectingToDeviceID) { + //重试连接 + [self performSelector:@selector(enqueueConnectToUSBDevice) withObject:nil afterDelay:ECOUSBChannelReconnectDelay]; + } + }else{ + self.connectedDeviceID = self.connectingToDeviceID; + self.connectedChannel = channel; + NSLog(@"<< [ECOUSBChannel] Connect to device #%@\n%@", channel.userInfo, self.connectedDeviceProperties); + //发送ping信息 + [self ping]; + } + }]; +} + +- (void)disconnectFromCurrentChannel { + if (self.connectedDeviceID && self.connectedChannel) { + [self.connectedChannel close]; + self.connectedChannel = nil; + } +} + +- (void)didDisconnectFromDevice:(NSNumber*)deviceID { + NSLog(@"<< [ECOUSBChannel] Disconnected from device:%@", deviceID); + if ([self.connectedDeviceID isEqualToNumber:deviceID]) { + [self willChangeValueForKey:@"connectedDeviceID"]; + self.connectedDeviceID = nil; + [self didChangeValueForKey:@"connectedDeviceID"]; + } +} + +#pragma mark - Send Messages +- (void)ping { + if (!self.connectedChannel) { + [self performSelector:@selector(ping) withObject:nil afterDelay:1.f]; + return; + } + uint32_t tagno = [self.connectedChannel.protocol newTag]; + ECOChannelDeviceInfo *deviceInfo = [ECOChannelDeviceInfo new]; + dispatch_data_t payload = ECOUSBChannelDispatchDataWithPayload(deviceInfo.ipAddress); + [self.connectedChannel sendFrameOfType:ECOUSBChannelType_Ping tag:tagno withPayload:payload callback:^(NSError *error) { + // [self performSelector:@selector(ping) withObject:nil afterDelay:1.f]; + }]; +} +- (void)sendMessage:(NSString *)message { +#if TARGET_OS_OSX + if (self.connectedChannel) { + [self.connectedChannel sendFrameOfType:ECOUSBChannelType_TextMessage tag:PTFrameNoTag withPayload:ECOUSBChannelDispatchDataWithPayload(message) callback:^(NSError *error) { + if (error) { + NSLog(@">> [ECOUSBChannel] Failed to send message: %@", error); + } + NSLog(@">> [ECOUSBChannel] you: %@", message); + }]; + } +#else + if (self.peerChannel) { + [self.peerChannel sendFrameOfType:ECOUSBChannelType_TextMessage tag:PTFrameNoTag withPayload:ECOUSBChannelDispatchDataWithPayload(message) callback:^(NSError *error) { + if (error) { + NSLog(@">> [ECOUSBChannel] Failed to send message: %@", error); + } + NSLog(@">> [ECOUSBChannel] you: %@", message); + }]; + } +#endif +} +#pragma mark - dispatch_data_t +static dispatch_data_t ECOUSBChannelDispatchDataWithPayload(id payload) { + if ([payload isKindOfClass:[NSString class]]) { + //字符串 + NSString *message = (NSString *)payload; + const char *utf8text = [message cStringUsingEncoding:NSUTF8StringEncoding]; + size_t length = strlen(utf8text); + ECOUSBChannelTextFrame *textFrame = CFAllocatorAllocate(nil, sizeof(ECOUSBChannelTextFrame) + length, 0); + memcpy(textFrame->utf8text, utf8text, length); + textFrame->length = htonl(length); + + return dispatch_data_create((const void*)textFrame, sizeof(ECOUSBChannelTextFrame) + length, nil, ^{ + CFAllocatorDeallocate(nil, textFrame); + }); + }else if ([payload isKindOfClass:[NSDictionary class]]) { + //字典 + NSDictionary *info = (NSDictionary *)payload; + return [info createReferencingDispatchData]; + } + return nil; +} +#pragma mark - PTChannelDelegate methods +// Invoked to accept an incoming frame on a channel. Reply NO ignore the +// incoming frame. If not implemented by the delegate, all frames are accepted. +- (BOOL)ioFrameChannel:(Lookin_PTChannel*)channel shouldAcceptFrameOfType:(uint32_t)type tag:(uint32_t)tag payloadSize:(uint32_t)payloadSize { +// NSLog(@"%s",__func__); +#if TARGET_OS_OSX + if (channel != self.peerChannel) { + // A previous channel that has been canceled but not yet ended. Ignore. + return NO; + } +#endif + return YES; +} + +// Invoked when a new frame has arrived on a channel. +- (void)ioFrameChannel:(Lookin_PTChannel*)channel didReceiveFrameOfType:(uint32_t)type tag:(uint32_t)tag payload:(Lookin_PTData*)payload { +// NSLog(@"%s",__func__); + if (type == ECOUSBChannelType_TextMessage) { + ECOUSBChannelTextFrame *textFrame = (ECOUSBChannelTextFrame *)payload.data; + textFrame->length = ntohl(textFrame->length); + NSString *message = [[NSString alloc] initWithBytes:textFrame->utf8text length:textFrame->length encoding:NSUTF8StringEncoding]; +// NSLog(@">> [ECOUSBChannel] [%@]: %@", channel.userInfo, message); + // 测试:自动会话 + [self sendMessage:message]; + }else if (type == ECOUSBChannelType_Ping) { +// NSLog(@">> [ECOUSBChannel] received ping: %@", channel.userInfo); + ECOUSBChannelTextFrame *textFrame = (ECOUSBChannelTextFrame *)payload.data; + textFrame->length = ntohl(textFrame->length); + NSString *ipAddress = [[NSString alloc] initWithBytes:textFrame->utf8text length:textFrame->length encoding:NSUTF8StringEncoding]; +// NSLog(@">> [ECOUSBChannel] [%@]: %@", channel.userInfo, ipAddress); + !self.attachBlock ?: self.attachBlock(ipAddress); + +// if (self.peerChannel) { +// [self.peerChannel sendFrameOfType:ECOUSBChannelType_Pong tag:tag withPayload:nil callback:nil]; +// } + }else if (type == ECOUSBChannelType_Pong) { +// NSLog(@">> [ECOUSBChannel] receive pong: %@", channel.userInfo); + } +} + +// Invoked when the channel closed. If it closed because of an error, *error* is +// a non-nil NSError object. +- (void)ioFrameChannel:(Lookin_PTChannel*)channel didEndWithError:(NSError*)error { +// NSLog(@"%s",__func__); + if (error) { +// NSLog(@">> [ECOUSBChannel] %@ end with error:%@", channel, error); + }else{ +// NSLog(@">> [ECOUSBChannel] Disconnected from %@", channel.userInfo); + } +#if TARGET_OS_OSX + if (self.connectedDeviceID && [self.connectedDeviceID isEqualToNumber:channel.userInfo]) { + [self didDisconnectFromDevice:self.connectedDeviceID]; + } + if (self.connectedChannel == channel) { +// NSLog(@">> [ECOUSBChannel] Disconnected from: %@", channel.userInfo); + self.connectedChannel = nil; + } +#endif +} + +// For listening channels, this method is invoked when a new connection has been +// accepted. +- (void)ioFrameChannel:(Lookin_PTChannel*)channel didAcceptConnection:(Lookin_PTChannel*)otherChannel fromAddress:(Lookin_PTAddress*)address { +// NSLog(@"%s",__func__); + if (self.peerChannel) { + [self.peerChannel cancel]; + } + + self.peerChannel = otherChannel; + self.peerChannel.userInfo = address; +// NSLog(@">> [ECOUSBChannel] Connected to %@", address); + //测试,回复消息 +// [self sendMessage:@"你好"]; +} + +@end diff --git a/Src/Main/Shared/LookinAppInfo.h b/Src/Main/Shared/LookinAppInfo.h index 1082d12d..4129e74a 100644 --- a/Src/Main/Shared/LookinAppInfo.h +++ b/Src/Main/Shared/LookinAppInfo.h @@ -54,6 +54,8 @@ typedef NS_ENUM(NSInteger, LookinAppInfoDevice) { @property(nonatomic, assign) double screenHeight; /// 是几倍的屏幕 @property(nonatomic, assign) double screenScale; +/// 是否为无线连接 +@property(nonatomic, assign) BOOL isWireless; - (BOOL)isEqualToAppInfo:(LookinAppInfo *)info; diff --git a/Src/Main/Shared/LookinAppInfo.m b/Src/Main/Shared/LookinAppInfo.m index 854c75c9..4415e83a 100644 --- a/Src/Main/Shared/LookinAppInfo.m +++ b/Src/Main/Shared/LookinAppInfo.m @@ -13,6 +13,7 @@ #import "LookinAppInfo.h" #if TARGET_OS_IPHONE #import "LKS_MultiplatformAdapter.h" +#import "LKS_ConnectionManager.h" #endif static NSString * const CodingKey_AppIcon = @"1"; @@ -38,6 +39,7 @@ - (id)copyWithZone:(NSZone *)zone { newAppInfo.screenHeight = self.screenHeight; newAppInfo.screenScale = self.screenScale; newAppInfo.appInfoIdentifier = self.appInfoIdentifier; + newAppInfo.isWireless = self.isWireless; return newAppInfo; } @@ -64,6 +66,7 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder { self.screenScale = [aDecoder decodeDoubleForKey:@"screenScale"]; self.appInfoIdentifier = [aDecoder decodeIntegerForKey:@"appInfoIdentifier"]; self.shouldUseCache = [aDecoder decodeBoolForKey:@"shouldUseCache"]; + self.isWireless = [aDecoder decodeBoolForKey:@"isWireless"]; } return self; } @@ -98,6 +101,7 @@ - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeDouble:self.screenScale forKey:@"screenScale"]; [aCoder encodeInteger:self.appInfoIdentifier forKey:@"appInfoIdentifier"]; [aCoder encodeBool:self.shouldUseCache forKey:@"shouldUseCache"]; + [aCoder encodeBool:self.isWireless forKey:@"isWireless"]; } + (BOOL)supportsSecureCoding { @@ -177,6 +181,10 @@ + (LookinAppInfo *)currentInfoWithScreenshot:(BOOL)hasScreenshot icon:(BOOL)hasI if (hasIcon) { info.appIcon = [self appIcon]; } + info.isWireless = LKS_ConnectionManager.sharedInstance.isWirelessConnnect; + if (info.isWireless) { + info.deviceDescription = [NSString stringWithFormat:@"ᯤ %@", info.deviceDescription]; + } return info; } diff --git a/Src/Main/Shared/LookinDefines.h b/Src/Main/Shared/LookinDefines.h index f00540de..e3eb3968 100644 --- a/Src/Main/Shared/LookinDefines.h +++ b/Src/Main/Shared/LookinDefines.h @@ -43,6 +43,15 @@ static const int LookinUSBDeviceIPv4PortNumberEnd = 47179; static const int LookinSimulatorIPv4PortNumberStart = 47164; static const int LookinSimulatorIPv4PortNumberEnd = 47169; +static const int LookinClientSockeListenPortNumber = 47180; +static const int LookinSocketAcceptPortNumber = 47181; +static const int LookinNetServicePortNumber = LookinSocketAcceptPortNumber; + +static NSString *const LookinNetServiceDomain = @""; +static NSString *const LookinNetServiceType = @"_Lookin._tcp"; +static NSString *const LookinNetServiceName = @""; +static NSTimeInterval const LookinNetServiceResolveAddressTimeout = 30; + enum { /// 确认两端是否可以响应通讯 LookinRequestTypePing = 200, From c335f2d6712f36c75525a1574054ff3351282798 Mon Sep 17 00:00:00 2001 From: mlch911 Date: Thu, 27 Jul 2023 12:42:18 +0800 Subject: [PATCH 02/13] Fix Bug: Wireless Connection --- Src/Main/Server/Connection/LKS_ConnectionManager.m | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Src/Main/Server/Connection/LKS_ConnectionManager.m b/Src/Main/Server/Connection/LKS_ConnectionManager.m index 35630b5f..eb8a58c6 100644 --- a/Src/Main/Server/Connection/LKS_ConnectionManager.m +++ b/Src/Main/Server/Connection/LKS_ConnectionManager.m @@ -112,6 +112,9 @@ - (void)startWirelessConnection { // 授权状态变更回调 self.wirelessChannel.authStateChangedBlock = ^(ECOChannelDeviceInfo *device, ECOAuthorizeResponseType authState) { NSLog(@"🚀 Lookin authStateChangedBlock device:%@ authState:%ld", device, authState); + if (authState == ECOAuthorizeResponseType_AllowAlways) { + weakSelf.wirelessDevice = device; + } }; // 请求授权状态认证回调 self.wirelessChannel.requestAuthBlock = ^(ECOChannelDeviceInfo *device, ECOAuthorizeResponseType authState) { From aabcd5ae39d17540595344d2c90f0d6b6ed5a0a3 Mon Sep 17 00:00:00 2001 From: mlch911 Date: Mon, 25 Sep 2023 20:16:26 +0800 Subject: [PATCH 03/13] Add Wireless subspec --- LookinServer.podspec | 12 +++++++++--- LookinShared.podspec | 12 ++++++++++-- Src/Main/Server/Connection/LKS_ConnectionManager.h | 4 ++++ Src/Main/Server/Connection/LKS_ConnectionManager.m | 13 ++++++++++++- Src/Main/Shared/LookinAppInfo.h | 2 ++ Src/Main/Shared/LookinAppInfo.m | 8 ++++++++ Src/Main/Shared/LookinDefines.h | 2 ++ 7 files changed, 47 insertions(+), 6 deletions(-) diff --git a/LookinServer.podspec b/LookinServer.podspec index ceddeab5..b0017c85 100644 --- a/LookinServer.podspec +++ b/LookinServer.podspec @@ -14,10 +14,9 @@ Pod::Spec.new do |spec| spec.framework = "UIKit" spec.requires_arc = true - spec.dependency 'CocoaAsyncSocket' - spec.subspec 'Core' do |ss| ss.source_files = ['Src/Main/**/*', 'Src/Base/**/*'] + ss.exclude_files = 'Src/Main/Shared/Channel/**/*' ss.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) SHOULD_COMPILE_LOOKIN_SERVER=1', 'SWIFT_ACTIVE_COMPILATION_CONDITIONS' => '$(inherited) SHOULD_COMPILE_LOOKIN_SERVER' @@ -39,7 +38,7 @@ Pod::Spec.new do |spec| 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) LOOKIN_SERVER_DISABLE_HOOK=1', } end - + # CocoaPods 不支持多个 subspecs 和 configurations 并列 # "pod 'LookinServer', :subspecs => ['Swift', 'NoHook'], :configurations => ['Debug']" is not supported by CocoaPods # https://github.com/QMUI/LookinServer/issues/134 @@ -52,4 +51,11 @@ Pod::Spec.new do |spec| } end + spec.subspec 'Wireless' do |ss| + ss.dependency 'LookinShared/Wireless' + ss.pod_target_xcconfig = { + 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) LOOKIN_SERVER_WIRELESS=1' + } + end + end diff --git a/LookinShared.podspec b/LookinShared.podspec index d864d725..0dbc6984 100644 --- a/LookinShared.podspec +++ b/LookinShared.podspec @@ -17,10 +17,18 @@ Pod::Spec.new do |spec| 'Src/Main/Shared/**/*', 'Src/Base/**/*' ] - - spec.dependency 'CocoaAsyncSocket' + spec.exclude_files = 'Src/Main/Shared/Channel/**/*' + spec.default_subspec = :none spec.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) SHOULD_COMPILE_LOOKIN_SERVER=1' } + + spec.subspec 'Wireless' do |ss| + ss.source_files = 'Src/Main/Shared/Channel/**/*' + ss.dependency 'CocoaAsyncSocket' + ss.pod_target_xcconfig = { + 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) LOOKIN_SERVER_WIRELESS=1' + } + end end diff --git a/Src/Main/Server/Connection/LKS_ConnectionManager.h b/Src/Main/Server/Connection/LKS_ConnectionManager.h index fc71cf0b..e8af56ce 100644 --- a/Src/Main/Server/Connection/LKS_ConnectionManager.h +++ b/Src/Main/Server/Connection/LKS_ConnectionManager.h @@ -20,13 +20,17 @@ extern NSString *const LKS_ConnectionDidEndNotificationName; @property(nonatomic, assign) BOOL applicationIsActive; +#if LOOKIN_SERVER_WIRELESS - (void)startWirelessConnection; - (void)endWirelessConnection; +#endif - (BOOL)isConnected; +#if LOOKIN_SERVER_WIRELESS - (BOOL)isWirelessConnnect; +#endif - (void)respond:(LookinConnectionResponseAttachment *)data requestType:(uint32_t)requestType tag:(uint32_t)tag; diff --git a/Src/Main/Server/Connection/LKS_ConnectionManager.m b/Src/Main/Server/Connection/LKS_ConnectionManager.m index eb8a58c6..d554c3b7 100644 --- a/Src/Main/Server/Connection/LKS_ConnectionManager.m +++ b/Src/Main/Server/Connection/LKS_ConnectionManager.m @@ -18,7 +18,9 @@ #import "LKS_MultiplatformAdapter.h" #import "ECOChannelManager.h" +#if LOOKIN_SERVER_WIRELESS @import CocoaAsyncSocket; +#endif NSString *const LKS_ConnectionDidEndNotificationName = @"LKS_ConnectionDidEndNotificationName"; @@ -61,11 +63,12 @@ - (instancetype)init { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleApplicationDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleWillResignActiveNotification) name:UIApplicationWillResignActiveNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleLocalInspect:) name:@"Lookin_2D" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleLocalInspect:) name:@"Lookin_3D" object:nil]; +#if LOOKIN_SERVER_WIRELESS [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(startWirelessConnection) name:@"Lookin_startWirelessConnection" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(endWirelessConnection) name:@"Lookin_endWirelessConnection" object:nil]; +#endif [[NSNotificationCenter defaultCenter] addObserverForName:@"Lookin_Export" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { [[LKS_ExportManager sharedInstance] exportAndShare]; }]; @@ -79,6 +82,7 @@ - (instancetype)init { return self; } +#if LOOKIN_SERVER_WIRELESS - (void)startWirelessConnection { self.hasStartWirelessConnnection = YES; if (!self.wirelessChannel) { @@ -156,6 +160,7 @@ - (void)endWirelessConnection { } self.wirelessChannel = nil; } +#endif - (void)_handleWillResignActiveNotification { self.applicationIsActive = NO; @@ -246,12 +251,18 @@ - (void)dealloc { } - (BOOL)isConnected { +#if LOOKIN_SERVER_WIRELESS return self.isWirelessConnnect || (self.peerChannel_ && self.peerChannel_.isConnected); +#else + return self.peerChannel_ && self.peerChannel_.isConnected; +#endif } +#if LOOKIN_SERVER_WIRELESS - (BOOL)isWirelessConnnect { return self.wirelessChannel.isConnected; } +#endif - (void)respond:(LookinConnectionResponseAttachment *)data requestType:(uint32_t)requestType tag:(uint32_t)tag { [self _sendData:data frameOfType:requestType tag:tag]; diff --git a/Src/Main/Shared/LookinAppInfo.h b/Src/Main/Shared/LookinAppInfo.h index 4129e74a..caa351be 100644 --- a/Src/Main/Shared/LookinAppInfo.h +++ b/Src/Main/Shared/LookinAppInfo.h @@ -54,8 +54,10 @@ typedef NS_ENUM(NSInteger, LookinAppInfoDevice) { @property(nonatomic, assign) double screenHeight; /// 是几倍的屏幕 @property(nonatomic, assign) double screenScale; +#if LOOKIN_SERVER_WIRELESS /// 是否为无线连接 @property(nonatomic, assign) BOOL isWireless; +#endif - (BOOL)isEqualToAppInfo:(LookinAppInfo *)info; diff --git a/Src/Main/Shared/LookinAppInfo.m b/Src/Main/Shared/LookinAppInfo.m index 4415e83a..895598c0 100644 --- a/Src/Main/Shared/LookinAppInfo.m +++ b/Src/Main/Shared/LookinAppInfo.m @@ -39,7 +39,9 @@ - (id)copyWithZone:(NSZone *)zone { newAppInfo.screenHeight = self.screenHeight; newAppInfo.screenScale = self.screenScale; newAppInfo.appInfoIdentifier = self.appInfoIdentifier; +#if LOOKIN_SERVER_WIRELESS newAppInfo.isWireless = self.isWireless; +#endif return newAppInfo; } @@ -66,7 +68,9 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder { self.screenScale = [aDecoder decodeDoubleForKey:@"screenScale"]; self.appInfoIdentifier = [aDecoder decodeIntegerForKey:@"appInfoIdentifier"]; self.shouldUseCache = [aDecoder decodeBoolForKey:@"shouldUseCache"]; +#if LOOKIN_SERVER_WIRELESS self.isWireless = [aDecoder decodeBoolForKey:@"isWireless"]; +#endif } return self; } @@ -101,7 +105,9 @@ - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeDouble:self.screenScale forKey:@"screenScale"]; [aCoder encodeInteger:self.appInfoIdentifier forKey:@"appInfoIdentifier"]; [aCoder encodeBool:self.shouldUseCache forKey:@"shouldUseCache"]; +#if LOOKIN_SERVER_WIRELESS [aCoder encodeBool:self.isWireless forKey:@"isWireless"]; +#endif } + (BOOL)supportsSecureCoding { @@ -181,10 +187,12 @@ + (LookinAppInfo *)currentInfoWithScreenshot:(BOOL)hasScreenshot icon:(BOOL)hasI if (hasIcon) { info.appIcon = [self appIcon]; } +#if LOOKIN_SERVER_WIRELESS info.isWireless = LKS_ConnectionManager.sharedInstance.isWirelessConnnect; if (info.isWireless) { info.deviceDescription = [NSString stringWithFormat:@"ᯤ %@", info.deviceDescription]; } +#endif return info; } diff --git a/Src/Main/Shared/LookinDefines.h b/Src/Main/Shared/LookinDefines.h index e3eb3968..5c9d9b9a 100644 --- a/Src/Main/Shared/LookinDefines.h +++ b/Src/Main/Shared/LookinDefines.h @@ -43,6 +43,7 @@ static const int LookinUSBDeviceIPv4PortNumberEnd = 47179; static const int LookinSimulatorIPv4PortNumberStart = 47164; static const int LookinSimulatorIPv4PortNumberEnd = 47169; +#if LOOKIN_SERVER_WIRELESS static const int LookinClientSockeListenPortNumber = 47180; static const int LookinSocketAcceptPortNumber = 47181; static const int LookinNetServicePortNumber = LookinSocketAcceptPortNumber; @@ -51,6 +52,7 @@ static NSString *const LookinNetServiceDomain = @""; static NSString *const LookinNetServiceType = @"_Lookin._tcp"; static NSString *const LookinNetServiceName = @""; static NSTimeInterval const LookinNetServiceResolveAddressTimeout = 30; +#endif enum { /// 确认两端是否可以响应通讯 From 1c0302dbdfb81f68ee5913c8015bdfcd7363d441 Mon Sep 17 00:00:00 2001 From: mlch911 Date: Mon, 13 Nov 2023 14:13:50 +0800 Subject: [PATCH 04/13] Update podspec --- LookinServer.podspec | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/LookinServer.podspec b/LookinServer.podspec index b0017c85..87544622 100644 --- a/LookinServer.podspec +++ b/LookinServer.podspec @@ -51,7 +51,19 @@ Pod::Spec.new do |spec| } end + spec.subspec 'SwiftAndWireless' do |ss| + ss.dependency 'LookinShared/Wireless' + ss.dependency 'LookinServer/Core' + ss.source_files = 'Src/Swift/**/*' + ss.pod_target_xcconfig = { + 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) LOOKIN_SERVER_SWIFT_ENABLED=1 LOOKIN_SERVER_DISABLE_HOOK=1', + 'SWIFT_ACTIVE_COMPILATION_CONDITIONS' => '$(inherited) LOOKIN_SERVER_SWIFT_ENABLED', + 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) LOOKIN_SERVER_WIRELESS=1' + } + end + spec.subspec 'Wireless' do |ss| + ss.dependency 'LookinServer/Core' ss.dependency 'LookinShared/Wireless' ss.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) LOOKIN_SERVER_WIRELESS=1' From 36cbb14fd7f913c3b7a3d6cf0bbd7cd3e2adc510 Mon Sep 17 00:00:00 2001 From: mlch911 Date: Thu, 14 Mar 2024 14:17:40 +0800 Subject: [PATCH 05/13] Fix Bug: podspec --- LookinServer.podspec | 3 +-- LookinShared.podspec | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/LookinServer.podspec b/LookinServer.podspec index 87544622..ee38f9da 100644 --- a/LookinServer.podspec +++ b/LookinServer.podspec @@ -56,9 +56,8 @@ Pod::Spec.new do |spec| ss.dependency 'LookinServer/Core' ss.source_files = 'Src/Swift/**/*' ss.pod_target_xcconfig = { - 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) LOOKIN_SERVER_SWIFT_ENABLED=1 LOOKIN_SERVER_DISABLE_HOOK=1', + 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) LOOKIN_SERVER_SWIFT_ENABLED=1 LOOKIN_SERVER_DISABLE_HOOK=1 LOOKIN_SERVER_WIRELESS=1', 'SWIFT_ACTIVE_COMPILATION_CONDITIONS' => '$(inherited) LOOKIN_SERVER_SWIFT_ENABLED', - 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) LOOKIN_SERVER_WIRELESS=1' } end diff --git a/LookinShared.podspec b/LookinShared.podspec index 0dbc6984..05bcad98 100644 --- a/LookinShared.podspec +++ b/LookinShared.podspec @@ -28,7 +28,7 @@ Pod::Spec.new do |spec| ss.source_files = 'Src/Main/Shared/Channel/**/*' ss.dependency 'CocoaAsyncSocket' ss.pod_target_xcconfig = { - 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) LOOKIN_SERVER_WIRELESS=1' + 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) SHOULD_COMPILE_LOOKIN_SERVER=1 LOOKIN_SERVER_WIRELESS=1' } end end From 5d89bdc59d650c08bb4bca4749680f167de0fb6d Mon Sep 17 00:00:00 2001 From: mlch911 Date: Fri, 23 Aug 2024 15:34:55 +0800 Subject: [PATCH 06/13] =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=97=A0=E7=BA=BF?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LookinDemo/OC_Pod/LookinDemoOC/AppDelegate.m | 3 + LookinDemo/OC_Pod/LookinDemoOC/Info.plist | 4 + .../Server/Connection/LKS_ConnectionManager.h | 4 +- .../Server/Connection/LKS_ConnectionManager.m | 36 +++-- .../Server/Connection/LKS_RequestHandler.h | 4 +- .../Server/Connection/LKS_RequestHandler.m | 140 +++++++++--------- .../Shared/Channel/ECOChannelDeviceInfo.h | 1 + .../Shared/Channel/ECOChannelDeviceInfo.m | 6 +- 8 files changed, 113 insertions(+), 85 deletions(-) diff --git a/LookinDemo/OC_Pod/LookinDemoOC/AppDelegate.m b/LookinDemo/OC_Pod/LookinDemoOC/AppDelegate.m index 48c9c2f9..d958b235 100644 --- a/LookinDemo/OC_Pod/LookinDemoOC/AppDelegate.m +++ b/LookinDemo/OC_Pod/LookinDemoOC/AppDelegate.m @@ -16,6 +16,9 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. +#if !SIMULATOR + [NSNotificationCenter.defaultCenter postNotificationName:@"Lookin_startWirelessConnection" object:nil]; +#endif return YES; } diff --git a/LookinDemo/OC_Pod/LookinDemoOC/Info.plist b/LookinDemo/OC_Pod/LookinDemoOC/Info.plist index 81ed29b7..7d1fc3e5 100644 --- a/LookinDemo/OC_Pod/LookinDemoOC/Info.plist +++ b/LookinDemo/OC_Pod/LookinDemoOC/Info.plist @@ -2,6 +2,10 @@ + NSBonjourServices + + _Lookin._tcp + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/Src/Main/Server/Connection/LKS_ConnectionManager.h b/Src/Main/Server/Connection/LKS_ConnectionManager.h index e8af56ce..29374845 100644 --- a/Src/Main/Server/Connection/LKS_ConnectionManager.h +++ b/Src/Main/Server/Connection/LKS_ConnectionManager.h @@ -32,9 +32,9 @@ extern NSString *const LKS_ConnectionDidEndNotificationName; - (BOOL)isWirelessConnnect; #endif -- (void)respond:(LookinConnectionResponseAttachment *)data requestType:(uint32_t)requestType tag:(uint32_t)tag; +- (void)respond:(LookinConnectionResponseAttachment *)data requestType:(uint32_t)requestType tag:(uint32_t)tag isWireless:(BOOL)isWireless; -- (void)pushData:(NSObject *)data type:(uint32_t)type; +- (void)pushData:(NSObject *)data type:(uint32_t)type isWireless:(BOOL)isWireless; @end diff --git a/Src/Main/Server/Connection/LKS_ConnectionManager.m b/Src/Main/Server/Connection/LKS_ConnectionManager.m index d554c3b7..43226b89 100644 --- a/Src/Main/Server/Connection/LKS_ConnectionManager.m +++ b/Src/Main/Server/Connection/LKS_ConnectionManager.m @@ -29,6 +29,7 @@ @interface LKS_ConnectionManager () @property(nonatomic, weak) Lookin_PTChannel *peerChannel_; @property(nonatomic, strong) LKS_RequestHandler *requestHandler; +@property(nonatomic, strong) LKS_RequestHandler *wirelessRequestHandler; @property(nonatomic, strong) ECOChannelManager *wirelessChannel; @property(nonatomic, strong) ECOChannelDeviceInfo *wirelessDevice; @@ -78,6 +79,7 @@ - (instancetype)init { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleGetLookinInfo:) name:@"GetLookinInfo" object:nil]; self.requestHandler = [LKS_RequestHandler new]; + self.wirelessRequestHandler = LKS_RequestHandler.wireless; } return self; } @@ -103,7 +105,7 @@ - (void)startWirelessConnection { object = unarchivedObject; } dispatch_async(dispatch_get_main_queue(), ^{ - [weakSelf.requestHandler handleRequestType:type.intValue tag:tag.intValue object:object]; + [weakSelf.wirelessRequestHandler handleRequestType:type.intValue tag:tag.intValue object:object]; }); }; // 设备连接变更 @@ -264,26 +266,30 @@ - (BOOL)isWirelessConnnect { } #endif -- (void)respond:(LookinConnectionResponseAttachment *)data requestType:(uint32_t)requestType tag:(uint32_t)tag { - [self _sendData:data frameOfType:requestType tag:tag]; +- (void)respond:(LookinConnectionResponseAttachment *)data requestType:(uint32_t)requestType tag:(uint32_t)tag isWireless:(BOOL)isWireless { + [self _sendData:data frameOfType:requestType tag:tag isWireless:isWireless]; } -- (void)pushData:(NSObject *)data type:(uint32_t)type { - [self _sendData:data frameOfType:type tag:0]; +- (void)pushData:(NSObject *)data type:(uint32_t)type isWireless:(BOOL)isWireless { + [self _sendData:data frameOfType:type tag:0 isWireless:isWireless]; } -- (void)_sendData:(NSObject *)data frameOfType:(uint32_t)frameOfType tag:(uint32_t)tag { +- (void)_sendData:(NSObject *)data frameOfType:(uint32_t)frameOfType tag:(uint32_t)tag isWireless:(BOOL)isWireless { NSData *archivedData = [NSKeyedArchiver archivedDataWithRootObject:data]; - if (self.peerChannel_) { - dispatch_data_t payload = [archivedData createReferencingDispatchData]; + if (isWireless) { + if (self.wirelessDevice.isConnected) { + [self.wirelessChannel sendPacket:archivedData extraInfo:@{@"tag": @(tag), @"type": @(frameOfType)} toDevice:self.wirelessDevice]; + } + } else { + if (self.peerChannel_) { + dispatch_data_t payload = [archivedData createReferencingDispatchData]; - [self.peerChannel_ sendFrameOfType:frameOfType tag:tag withPayload:payload callback:^(NSError *error) { - if (error) { - } - }]; - } else if (self.wirelessDevice.isConnected) { - [self.wirelessChannel sendPacket:archivedData extraInfo:@{@"tag": @(tag), @"type": @(frameOfType)} toDevice:self.wirelessDevice]; - } + [self.peerChannel_ sendFrameOfType:frameOfType tag:tag withPayload:payload callback:^(NSError *error) { + if (error) { + } + }]; + } + } } #pragma mark - Lookin_PTChannelDelegate diff --git a/Src/Main/Server/Connection/LKS_RequestHandler.h b/Src/Main/Server/Connection/LKS_RequestHandler.h index 52ebe712..e8692c7b 100644 --- a/Src/Main/Server/Connection/LKS_RequestHandler.h +++ b/Src/Main/Server/Connection/LKS_RequestHandler.h @@ -1,4 +1,4 @@ -#ifdef SHOULD_COMPILE_LOOKIN_SERVER +#ifdef SHOULD_COMPILE_LOOKIN_SERVER // // LKS_RequestHandler.h @@ -12,6 +12,8 @@ @interface LKS_RequestHandler : NSObject ++ (instancetype)wireless; + - (BOOL)canHandleRequestType:(uint32_t)requestType; - (void)handleRequestType:(uint32_t)requestType tag:(uint32_t)tag object:(id)object; diff --git a/Src/Main/Server/Connection/LKS_RequestHandler.m b/Src/Main/Server/Connection/LKS_RequestHandler.m index c9d1572d..536c91fc 100644 --- a/Src/Main/Server/Connection/LKS_RequestHandler.m +++ b/Src/Main/Server/Connection/LKS_RequestHandler.m @@ -1,4 +1,4 @@ -#ifdef SHOULD_COMPILE_LOOKIN_SERVER +#ifdef SHOULD_COMPILE_LOOKIN_SERVER // // LKS_RequestHandler.m @@ -31,6 +31,8 @@ @interface LKS_RequestHandler () @property(nonatomic, strong) NSMutableSet *activeDetailHandlers; +@property(nonatomic, assign) BOOL isWireless; + @end @implementation LKS_RequestHandler { @@ -54,12 +56,18 @@ - (instancetype)init { @(LookinRequestTypeModifyRecognizerEnable), @(LookinPush_CanceHierarchyDetails), nil]; - + self.activeDetailHandlers = [NSMutableSet set]; } return self; } ++ (instancetype)wireless { + LKS_RequestHandler *handler = self.new; + handler.isWireless = YES; + return handler; +} + - (BOOL)canHandleRequestType:(uint32_t)requestType { if ([_validRequestTypes containsObject:@(requestType)]) { return YES; @@ -72,10 +80,10 @@ - (void)handleRequestType:(uint32_t)requestType tag:(uint32_t)tag object:(id)obj LookinConnectionResponseAttachment *responseAttachment = [LookinConnectionResponseAttachment new]; // 当 app 处于后台时,可能可以执行代码也可能不能执行代码,如果运气好了可以执行代码,则这里直接主动使用 appIsInBackground 标识 app 处于后台,不要让 Lookin 客户端傻傻地等待超时了 if (![LKS_ConnectionManager sharedInstance].applicationIsActive) { - responseAttachment.appIsInBackground = YES; + responseAttachment.appIsInBackground = YES; } - [[LKS_ConnectionManager sharedInstance] respond:responseAttachment requestType:requestType tag:tag]; - + [[LKS_ConnectionManager sharedInstance] respond:responseAttachment requestType:requestType tag:tag isWireless:self.isWireless]; + } else if (requestType == LookinRequestTypeApp) { // 请求可用设备信息 if (![object isKindOfClass:[NSDictionary class]]) { @@ -85,13 +93,13 @@ - (void)handleRequestType:(uint32_t)requestType tag:(uint32_t)tag object:(id)obj NSDictionary *params = object; BOOL needImages = ((NSNumber *)params[@"needImages"]).boolValue; NSArray *localIdentifiers = params[@"local"]; - + LookinAppInfo *appInfo = [LookinAppInfo currentInfoWithScreenshot:needImages icon:needImages localIdentifiers:localIdentifiers]; - + LookinConnectionResponseAttachment *responseAttachment = [LookinConnectionResponseAttachment new]; responseAttachment.data = appInfo; - [[LKS_ConnectionManager sharedInstance] respond:responseAttachment requestType:requestType tag:tag]; - + [[LKS_ConnectionManager sharedInstance] respond:responseAttachment requestType:requestType tag:tag isWireless:self.isWireless]; + } else if (requestType == LookinRequestTypeHierarchy) { // 从 LookinClient 1.0.4 开始有这个参数,之前是 nil NSString *clientVersion = nil; @@ -102,11 +110,11 @@ - (void)handleRequestType:(uint32_t)requestType tag:(uint32_t)tag object:(id)obj clientVersion = version; } } - + LookinConnectionResponseAttachment *responseAttachment = [LookinConnectionResponseAttachment new]; responseAttachment.data = [LookinHierarchyInfo staticInfoWithLookinVersion:clientVersion]; - [[LKS_ConnectionManager sharedInstance] respond:responseAttachment requestType:requestType tag:tag]; - + [[LKS_ConnectionManager sharedInstance] respond:responseAttachment requestType:requestType tag:tag isWireless:self.isWireless]; + } else if (requestType == LookinRequestTypeInbuiltAttrModification) { // 请求修改某个属性 [LKS_InbuiltAttrModificationHandler handleModification:object completion:^(LookinDisplayItemDetail *data, NSError *error) { @@ -116,9 +124,9 @@ - (void)handleRequestType:(uint32_t)requestType tag:(uint32_t)tag object:(id)obj } else { attachment.data = data; } - [[LKS_ConnectionManager sharedInstance] respond:attachment requestType:requestType tag:tag]; + [[LKS_ConnectionManager sharedInstance] respond:attachment requestType:requestType tag:tag isWireless:self.isWireless]; }]; - + } else if (requestType == LookinRequestTypeCustomAttrModification) { BOOL succ = [LKS_CustomAttrModificationHandler handleModification:object]; if (succ) { @@ -126,7 +134,7 @@ - (void)handleRequestType:(uint32_t)requestType tag:(uint32_t)tag object:(id)obj } else { [self _submitResponseWithError:LookinErr_Inner requestType:requestType tag:tag]; } - + } else if (requestType == LookinRequestTypeAttrModificationPatch) { NSArray *tasks = object; NSUInteger dataTotalCount = tasks.count; @@ -135,39 +143,39 @@ - (void)handleRequestType:(uint32_t)requestType tag:(uint32_t)tag object:(id)obj attrAttachment.data = data; attrAttachment.dataTotalCount = dataTotalCount; attrAttachment.currentDataCount = 1; - [[LKS_ConnectionManager sharedInstance] respond:attrAttachment requestType:LookinRequestTypeAttrModificationPatch tag:tag]; + [[LKS_ConnectionManager sharedInstance] respond:attrAttachment requestType:LookinRequestTypeAttrModificationPatch tag:tag isWireless:self.isWireless]; }]; - + } else if (requestType == LookinRequestTypeHierarchyDetails) { NSArray *packages = object; NSUInteger responsesDataTotalCount = [packages lookin_reduceInteger:^NSInteger(NSInteger accumulator, NSUInteger idx, LookinStaticAsyncUpdateTasksPackage *package) { accumulator += package.tasks.count; return accumulator; } initialAccumlator:0]; - + LKS_HierarchyDetailsHandler *handler = [LKS_HierarchyDetailsHandler new]; [self.activeDetailHandlers addObject:handler]; - + [handler startWithPackages:packages block:^(NSArray *details) { LookinConnectionResponseAttachment *attachment = [LookinConnectionResponseAttachment new]; attachment.data = details; attachment.dataTotalCount = responsesDataTotalCount; attachment.currentDataCount = details.count; - [[LKS_ConnectionManager sharedInstance] respond:attachment requestType:LookinRequestTypeHierarchyDetails tag:tag]; - + [[LKS_ConnectionManager sharedInstance] respond:attachment requestType:LookinRequestTypeHierarchyDetails tag:tag isWireless:self.isWireless]; + } finishedBlock:^{ [self.activeDetailHandlers removeObject:handler]; }]; - + } else if (requestType == LookinRequestTypeFetchObject) { unsigned long oid = ((NSNumber *)object).unsignedLongValue; NSObject *object = [NSObject lks_objectWithOid:oid]; LookinObject *lookinObj = [LookinObject instanceWithObject:object]; - + LookinConnectionResponseAttachment *attach = [LookinConnectionResponseAttachment new]; attach.data = lookinObj; - [[LKS_ConnectionManager sharedInstance] respond:attach requestType:requestType tag:tag]; - + [[LKS_ConnectionManager sharedInstance] respond:attach requestType:requestType tag:tag isWireless:self.isWireless]; + } else if (requestType == LookinRequestTypeAllAttrGroups) { unsigned long oid = ((NSNumber *)object).unsignedLongValue; CALayer *layer = (CALayer *)[NSObject lks_objectWithOid:oid]; @@ -175,10 +183,10 @@ - (void)handleRequestType:(uint32_t)requestType tag:(uint32_t)tag object:(id)obj [self _submitResponseWithError:LookinErr_ObjNotFound requestType:LookinRequestTypeAllAttrGroups tag:tag]; return; } - + NSArray *list = [LKS_AttrGroupsMaker attrGroupsForLayer:layer]; [self _submitResponseWithData:list requestType:LookinRequestTypeAllAttrGroups tag:tag]; - + } else if (requestType == LookinRequestTypeAllSelectorNames) { if (![object isKindOfClass:[NSDictionary class]]) { [self _submitResponseWithError:LookinErr_Inner requestType:requestType tag:tag]; @@ -192,10 +200,10 @@ - (void)handleRequestType:(uint32_t)requestType tag:(uint32_t)tag object:(id)obj [self _submitResponseWithError:LookinErrorMake(errorMsg, @"") requestType:requestType tag:tag]; return; } - + NSArray *selNames = [self _methodNameListForClass:targetClass hasArg:hasArg]; [self _submitResponseWithData:selNames requestType:requestType tag:tag]; - + } else if (requestType == LookinRequestTypeInvokeMethod) { if (![object isKindOfClass:[NSDictionary class]]) { [self _submitResponseWithError:LookinErr_Inner requestType:requestType tag:tag]; @@ -213,7 +221,7 @@ - (void)handleRequestType:(uint32_t)requestType tag:(uint32_t)tag object:(id)obj [self _submitResponseWithError:LookinErr_ObjNotFound requestType:requestType tag:tag]; return; } - + SEL targetSelector = NSSelectorFromString(text); if (targetSelector && [targerObj respondsToSelector:targetSelector]) { NSString *resultDescription; @@ -236,13 +244,13 @@ - (void)handleRequestType:(uint32_t)requestType tag:(uint32_t)tag object:(id)obj NSString *errMsg = [NSString stringWithFormat:LKS_Localized(@"%@ doesn't have an instance method called \"%@\"."), NSStringFromClass(targerObj.class), text]; [self _submitResponseWithError:LookinErrorMake(errMsg, @"") requestType:requestType tag:tag]; } - + } else if (requestType == LookinPush_CanceHierarchyDetails) { [self.activeDetailHandlers enumerateObjectsUsingBlock:^(LKS_HierarchyDetailsHandler * _Nonnull handler, BOOL * _Nonnull stop) { [handler cancel]; }]; [self.activeDetailHandlers removeAllObjects]; - + } else if (requestType == LookinRequestTypeFetchImageViewImage) { if (![object isKindOfClass:[NSNumber class]]) { [self _submitResponseWithError:LookinErr_Inner requestType:requestType tag:tag]; @@ -261,7 +269,7 @@ - (void)handleRequestType:(uint32_t)requestType tag:(uint32_t)tag object:(id)obj UIImage *image = imageView.image; NSData *imageData = [image lookin_data]; [self _submitResponseWithData:imageData requestType:requestType tag:tag]; - + } else if (requestType == LookinRequestTypeModifyRecognizerEnable) { if (![object isKindOfClass:[NSDictionary class]]) { [self _submitResponseWithError:LookinErr_Inner requestType:requestType tag:tag]; @@ -270,7 +278,7 @@ - (void)handleRequestType:(uint32_t)requestType tag:(uint32_t)tag object:(id)obj NSDictionary *params = object; unsigned long recognizerOid = ((NSNumber *)params[@"oid"]).unsignedLongValue; BOOL shouldBeEnabled = ((NSNumber *)params[@"enable"]).boolValue; - + UIGestureRecognizer *recognizer = (UIGestureRecognizer *)[NSObject lks_objectWithOid:recognizerOid]; if (!recognizer) { [self _submitResponseWithError:LookinErr_ObjNotFound requestType:requestType tag:tag]; @@ -291,21 +299,21 @@ - (void)handleRequestType:(uint32_t)requestType tag:(uint32_t)tag object:(id)obj - (NSArray *)_methodNameListForClass:(Class)aClass hasArg:(BOOL)hasArg { NSSet *prefixesToVoid = [NSSet setWithObjects:@"_", @"CA_", @"cpl", @"mf_", @"vs_", @"pep_", @"isNS", @"avkit_", @"PG_", @"px_", @"pl_", @"nsli_", @"pu_", @"pxg_", nil]; NSMutableArray *array = [NSMutableArray array]; - + Class currentClass = aClass; while (currentClass) { NSString *className = NSStringFromClass(currentClass); BOOL isSystemClass = ([className hasPrefix:@"UI"] || [className hasPrefix:@"CA"] || [className hasPrefix:@"NS"]); - + unsigned int methodCount = 0; Method *methods = class_copyMethodList(currentClass, &methodCount); for (unsigned int i = 0; i < methodCount; i++) { NSString *selName = NSStringFromSelector(method_getName(methods[i])); - + if (!hasArg && [selName containsString:@":"]) { continue; } - + if (isSystemClass) { BOOL invalid = [prefixesToVoid lookin_any:^BOOL(NSString *prefix) { return [selName hasPrefix:prefix]; @@ -331,24 +339,24 @@ - (void)_handleInvokeWithObject:(NSObject *)obj selector:(SEL)selector resultDes *error = LookinErrorMake(LKS_Localized(@"Lookin doesn't support invoking methods with arguments yet."), @""); return; } - + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setTarget:obj]; [invocation setSelector:selector]; [invocation invoke]; const char *returnType = [signature methodReturnType]; - - + + if (strcmp(returnType, @encode(void)) == 0) { //void, do nothing *description = LookinStringFlag_VoidReturn; - + } else if (strcmp(returnType, @encode(char)) == 0) { char charValue; [invocation getReturnValue:&charValue]; *description = [NSString stringWithFormat:@"%@", @(charValue)]; - + } else if (strcmp(returnType, @encode(int)) == 0) { int intValue; [invocation getReturnValue:&intValue]; @@ -359,7 +367,7 @@ - (void)_handleInvokeWithObject:(NSObject *)obj selector:(SEL)selector resultDes } else { *description = [NSString stringWithFormat:@"%@", @(intValue)]; } - + } else if (strcmp(returnType, @encode(short)) == 0) { short shortValue; [invocation getReturnValue:&shortValue]; @@ -370,7 +378,7 @@ - (void)_handleInvokeWithObject:(NSObject *)obj selector:(SEL)selector resultDes } else { *description = [NSString stringWithFormat:@"%@", @(shortValue)]; } - + } else if (strcmp(returnType, @encode(long)) == 0) { long longValue; [invocation getReturnValue:&longValue]; @@ -383,7 +391,7 @@ - (void)_handleInvokeWithObject:(NSObject *)obj selector:(SEL)selector resultDes } else { *description = [NSString stringWithFormat:@"%@", @(longValue)]; } - + } else if (strcmp(returnType, @encode(long long)) == 0) { long long longLongValue; [invocation getReturnValue:&longLongValue]; @@ -394,7 +402,7 @@ - (void)_handleInvokeWithObject:(NSObject *)obj selector:(SEL)selector resultDes } else { *description = [NSString stringWithFormat:@"%@", @(longLongValue)]; } - + } else if (strcmp(returnType, @encode(unsigned char)) == 0) { unsigned char ucharValue; [invocation getReturnValue:&ucharValue]; @@ -403,7 +411,7 @@ - (void)_handleInvokeWithObject:(NSObject *)obj selector:(SEL)selector resultDes } else { *description = [NSString stringWithFormat:@"%@", @(ucharValue)]; } - + } else if (strcmp(returnType, @encode(unsigned int)) == 0) { unsigned int uintValue; [invocation getReturnValue:&uintValue]; @@ -412,7 +420,7 @@ - (void)_handleInvokeWithObject:(NSObject *)obj selector:(SEL)selector resultDes } else { *description = [NSString stringWithFormat:@"%@", @(uintValue)]; } - + } else if (strcmp(returnType, @encode(unsigned short)) == 0) { unsigned short ushortValue; [invocation getReturnValue:&ushortValue]; @@ -421,7 +429,7 @@ - (void)_handleInvokeWithObject:(NSObject *)obj selector:(SEL)selector resultDes } else { *description = [NSString stringWithFormat:@"%@", @(ushortValue)]; } - + } else if (strcmp(returnType, @encode(unsigned long)) == 0) { unsigned long ulongValue; [invocation getReturnValue:&ulongValue]; @@ -430,7 +438,7 @@ - (void)_handleInvokeWithObject:(NSObject *)obj selector:(SEL)selector resultDes } else { *description = [NSString stringWithFormat:@"%@", @(ulongValue)]; } - + } else if (strcmp(returnType, @encode(unsigned long long)) == 0) { unsigned long long ulongLongValue; [invocation getReturnValue:&ulongLongValue]; @@ -439,7 +447,7 @@ - (void)_handleInvokeWithObject:(NSObject *)obj selector:(SEL)selector resultDes } else { *description = [NSString stringWithFormat:@"%@", @(ulongLongValue)]; } - + } else if (strcmp(returnType, @encode(float)) == 0) { float floatValue; [invocation getReturnValue:&floatValue]; @@ -450,7 +458,7 @@ - (void)_handleInvokeWithObject:(NSObject *)obj selector:(SEL)selector resultDes } else { *description = [NSString stringWithFormat:@"%@", @(floatValue)]; } - + } else if (strcmp(returnType, @encode(double)) == 0) { double doubleValue; [invocation getReturnValue:&doubleValue]; @@ -461,22 +469,22 @@ - (void)_handleInvokeWithObject:(NSObject *)obj selector:(SEL)selector resultDes } else { *description = [NSString stringWithFormat:@"%@", @(doubleValue)]; } - + } else if (strcmp(returnType, @encode(BOOL)) == 0) { BOOL boolValue; [invocation getReturnValue:&boolValue]; *description = boolValue ? @"YES" : @"NO"; - + } else if (strcmp(returnType, @encode(SEL)) == 0) { SEL selValue; [invocation getReturnValue:&selValue]; *description = [NSString stringWithFormat:@"SEL(%@)", NSStringFromSelector(selValue)]; - + } else if (strcmp(returnType, @encode(Class)) == 0) { Class classValue; [invocation getReturnValue:&classValue]; *description = [NSString stringWithFormat:@"<%@>", NSStringFromClass(classValue)]; - + } else if (strcmp(returnType, @encode(CGPoint)) == 0) { CGPoint targetValue; [invocation getReturnValue:&targetValue]; @@ -496,22 +504,22 @@ - (void)_handleInvokeWithObject:(NSObject *)obj selector:(SEL)selector resultDes CGRect rectValue; [invocation getReturnValue:&rectValue]; *description = NSStringFromCGRect(rectValue); - + } else if (strcmp(returnType, @encode(CGAffineTransform)) == 0) { CGAffineTransform rectValue; [invocation getReturnValue:&rectValue]; *description = NSStringFromCGAffineTransform(rectValue); - + } else if (strcmp(returnType, @encode(UIEdgeInsets)) == 0) { UIEdgeInsets targetValue; [invocation getReturnValue:&targetValue]; *description = NSStringFromUIEdgeInsets(targetValue); - + } else if (strcmp(returnType, @encode(UIOffset)) == 0) { UIOffset targetValue; [invocation getReturnValue:&targetValue]; *description = NSStringFromUIOffset(targetValue); - + } else { if (@available(iOS 11.0, tvOS 11.0, *)) { if (strcmp(returnType, @encode(NSDirectionalEdgeInsets)) == 0) { @@ -521,15 +529,15 @@ - (void)_handleInvokeWithObject:(NSObject *)obj selector:(SEL)selector resultDes return; } } - + NSString *argType_string = [[NSString alloc] lookin_safeInitWithUTF8String:returnType]; if ([argType_string hasPrefix:@"@"] || [argType_string hasPrefix:@"^{"]) { __unsafe_unretained id returnObjValue; [invocation getReturnValue:&returnObjValue]; - + if (returnObjValue) { *description = [NSString stringWithFormat:@"%@", returnObjValue]; - + LookinObject *parsedLookinObj = [LookinObject instanceWithObject:returnObjValue]; *resultObject = parsedLookinObj; } else { @@ -544,13 +552,13 @@ - (void)_handleInvokeWithObject:(NSObject *)obj selector:(SEL)selector resultDes - (void)_submitResponseWithError:(NSError *)error requestType:(uint32_t)requestType tag:(uint32_t)tag { LookinConnectionResponseAttachment *attachment = [LookinConnectionResponseAttachment new]; attachment.error = error; - [[LKS_ConnectionManager sharedInstance] respond:attachment requestType:requestType tag:tag]; + [[LKS_ConnectionManager sharedInstance] respond:attachment requestType:requestType tag:tag isWireless:self.isWireless]; } - (void)_submitResponseWithData:(NSObject *)data requestType:(uint32_t)requestType tag:(uint32_t)tag { LookinConnectionResponseAttachment *attachment = [LookinConnectionResponseAttachment new]; attachment.data = data; - [[LKS_ConnectionManager sharedInstance] respond:attachment requestType:requestType tag:tag]; + [[LKS_ConnectionManager sharedInstance] respond:attachment requestType:requestType tag:tag isWireless:self.isWireless]; } @end diff --git a/Src/Main/Shared/Channel/ECOChannelDeviceInfo.h b/Src/Main/Shared/Channel/ECOChannelDeviceInfo.h index 66ffb13a..8d21f4c6 100644 --- a/Src/Main/Shared/Channel/ECOChannelDeviceInfo.h +++ b/Src/Main/Shared/Channel/ECOChannelDeviceInfo.h @@ -12,6 +12,7 @@ typedef NS_ENUM(NSInteger, ECODeviceType) { ECODeviceType_Simulator = 0, //模拟器 ECODeviceType_Device, //真机 + ECODeviceType_iPad_Device, //iPad真机 ECODeviceType_MacApp, //Mac客户端 }; diff --git a/Src/Main/Shared/Channel/ECOChannelDeviceInfo.m b/Src/Main/Shared/Channel/ECOChannelDeviceInfo.m index 45fa6fbf..f2a63c9e 100644 --- a/Src/Main/Shared/Channel/ECOChannelDeviceInfo.m +++ b/Src/Main/Shared/Channel/ECOChannelDeviceInfo.m @@ -107,7 +107,11 @@ - (void)setupIOSDeviceInfo { #if TARGET_IPHONE_SIMULATOR self.deviceType = ECODeviceType_Simulator; #else - self.deviceType = ECODeviceType_Device; + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + self.deviceType = ECODeviceType_iPad_Device; + } else { + self.deviceType = ECODeviceType_Device; + } #endif //uuid // self.uuid = [[NSUUID UUID] UUIDString]; From 92674be4273594b95371147427d4f78033ce977d Mon Sep 17 00:00:00 2001 From: mlch911 Date: Fri, 23 Aug 2024 16:24:28 +0800 Subject: [PATCH 07/13] =?UTF-8?q?Update:=20=E6=A8=A1=E6=8B=9F=E5=99=A8?= =?UTF-8?q?=E4=B8=8D=E5=BA=94=E8=AF=A5=E5=90=AF=E5=8A=A8=E6=97=A0=E7=BA=BF?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LookinDemo/OC_Pod/LookinDemoOC/AppDelegate.m | 2 +- Src/Main/Server/Connection/LKS_ConnectionManager.m | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/LookinDemo/OC_Pod/LookinDemoOC/AppDelegate.m b/LookinDemo/OC_Pod/LookinDemoOC/AppDelegate.m index d958b235..1714603b 100644 --- a/LookinDemo/OC_Pod/LookinDemoOC/AppDelegate.m +++ b/LookinDemo/OC_Pod/LookinDemoOC/AppDelegate.m @@ -16,7 +16,7 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. -#if !SIMULATOR +#if !TARGET_OS_SIMULATOR [NSNotificationCenter.defaultCenter postNotificationName:@"Lookin_startWirelessConnection" object:nil]; #endif return YES; diff --git a/Src/Main/Server/Connection/LKS_ConnectionManager.m b/Src/Main/Server/Connection/LKS_ConnectionManager.m index 43226b89..e4011e0c 100644 --- a/Src/Main/Server/Connection/LKS_ConnectionManager.m +++ b/Src/Main/Server/Connection/LKS_ConnectionManager.m @@ -86,6 +86,10 @@ - (instancetype)init { #if LOOKIN_SERVER_WIRELESS - (void)startWirelessConnection { +#if TARGET_OS_SIMULATOR + NSLog(@"LookinServer - warning: you should not start Wireless Connection on Simulator. We wouldn't start it."); + return; +#endif self.hasStartWirelessConnnection = YES; if (!self.wirelessChannel) { #if TARGET_OS_IPHONE From 6ba8231d589c7404064ca8d571c49746a9f67366 Mon Sep 17 00:00:00 2001 From: hongbo <1049145827@qq.com> Date: Sat, 18 Jul 2026 16:41:14 +0800 Subject: [PATCH 08/13] Harden opt-in wireless inspection --- .gitignore | 1 + LookinServer.podspec | 6 +- Package.swift | 3 + README.md | 48 +++++++++ .../Server/Connection/LKS_ConnectionManager.m | 33 +++--- .../Channel/Bonjour/ECONetServiceBrowser.m | 13 +-- Src/Main/Shared/Channel/ECOChannelAppInfo.m | 4 + .../Shared/Channel/ECOChannelDeviceInfo.m | 24 ++++- Src/Main/Shared/Channel/ECOSocketChannel.m | 102 ++++++++++++++++-- UPSTREAM_PATCHES.md | 1 + 10 files changed, 199 insertions(+), 36 deletions(-) diff --git a/.gitignore b/.gitignore index 681ad9bf..61aaa535 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,7 @@ Temporary Items ### SwiftPackageManager ### Packages .build/ +Package.resolved xcuserdata DerivedData/ #*.xcodeproj diff --git a/LookinServer.podspec b/LookinServer.podspec index ee38f9da..d60ea198 100644 --- a/LookinServer.podspec +++ b/LookinServer.podspec @@ -52,12 +52,10 @@ Pod::Spec.new do |spec| end spec.subspec 'SwiftAndWireless' do |ss| + ss.dependency 'LookinServer/Swift' ss.dependency 'LookinShared/Wireless' - ss.dependency 'LookinServer/Core' - ss.source_files = 'Src/Swift/**/*' ss.pod_target_xcconfig = { - 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) LOOKIN_SERVER_SWIFT_ENABLED=1 LOOKIN_SERVER_DISABLE_HOOK=1 LOOKIN_SERVER_WIRELESS=1', - 'SWIFT_ACTIVE_COMPILATION_CONDITIONS' => '$(inherited) LOOKIN_SERVER_SWIFT_ENABLED', + 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) LOOKIN_SERVER_WIRELESS=1' } end diff --git a/Package.swift b/Package.swift index e2b060e5..b6551502 100644 --- a/Package.swift +++ b/Package.swift @@ -28,6 +28,9 @@ let package = Package( name: "LookinServer", dependencies: [.target(name: "LookinServerSwift")], path: "Src/Main", + // Wireless transport is intentionally CocoaPods-only for now. Keeping it + // out of the default product prevents local-network access in normal SPM builds. + exclude: ["Shared/Channel"], publicHeadersPath: "", cSettings: [ .headerSearchPath("**"), diff --git a/README.md b/README.md index 11bdc79c..a0c080a4 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,46 @@ To use Lookin macOS app, you need to integrate LookinServer (iOS Framework of Lo ## via Swift Package Manager: `https://github.com/QMUI/LookinServer/` +## Experimental wireless connection + +Wireless inspection is opt-in, CocoaPods-only, and intended for Debug builds on a trusted local network. The transport is not encrypted. The iOS app asks for confirmation before accepting a new Mac, and remembered devices are validated independently on both sides. + +```ruby +# Swift + wireless inspection +pod 'LookinServer', + :git => 'https://github.com/nova286/LookinServer.git', + :subspecs => ['SwiftAndWireless'], + :configurations => ['Debug'] + +# Objective-C + wireless inspection +# pod 'LookinServer', :git => 'https://github.com/nova286/LookinServer.git', +# :subspecs => ['Wireless'], :configurations => ['Debug'] +``` + +Add the following declarations to the inspected app's `Info.plist`: + +```xml +NSLocalNetworkUsageDescription +Connect to Lookin on your Mac for local UI inspection. +NSBonjourServices + + _Lookin._tcp + +``` + +Start discovery explicitly after the app is active. This is intentionally not automatic, so merely linking the subspec does not request local-network permission: + +```swift +#if DEBUG +NotificationCenter.default.post( + name: Notification.Name("Lookin_startWirelessConnection"), + object: nil +) +#endif +``` + +Post `Lookin_endWirelessConnection` to stop it. The default `Core`/`Swift` subspecs and the default Swift Package Manager product do not compile the wireless transport or CocoaAsyncSocket. + ## Experimental SwiftUI semantic hierarchy The `codex/swiftui-attached-macro` branch can expose opt-in SwiftUI semantic nodes through Lookin's existing hierarchy and custom-attribute protocol. The matching macOS client fork adds a dedicated hierarchy mode and automatic refresh after edits. @@ -103,6 +143,14 @@ Lookin 可以查看与修改 iOS App 里的 UI 对象,类似于 Xcode 自带 ## 通过 Swift Package Manager: `https://github.com/QMUI/LookinServer/` +## 实验性无线连接 + +无线检查能力是显式可选的,目前仅支持 CocoaPods,且只应在可信局域网的 Debug 构建中启用。传输内容没有加密;首次连接新 Mac 时 iOS App 会要求用户确认,“始终允许”会在两端分别校验已记住的设备身份。 + +Swift 项目使用 `SwiftAndWireless` subspec,Objective-C 项目使用 `Wireless` subspec,并继续通过 `:configurations => ['Debug']` 限制构建配置。示例见上方英文文档。 + +被检查 App 的 `Info.plist` 必须声明 `NSLocalNetworkUsageDescription`,并在 `NSBonjourServices` 中加入 `_Lookin._tcp`。App 启动并进入 active 状态后,发送 `Lookin_startWirelessConnection` 通知才会开始发现设备;发送 `Lookin_endWirelessConnection` 可停止。该能力不会自动启动,默认 `Core`/`Swift` subspec 与默认 SPM product 都不会编译无线通道或引入 CocoaAsyncSocket。 + ## 实验性 SwiftUI 语义层级 `codex/swiftui-attached-macro` 分支支持把 SwiftUI 语义节点接入 Lookin 现有层级与自定义属性协议;配套的 macOS 客户端 fork 提供独立的 SwiftUI 层级模式,并在修改属性后自动刷新。使用 `@LookinInspectable` 标记 View 类型后,Debug 构建以及显式定义 `STAGING` 的 Staging 构建会自动包装 `body`、为每个渲染实例生成唯一 ID,并从最近的标记父节点继承语义层级。每棵层级只需在根部保留一次 `lookinSwiftUIInspector()`;只有需要自定义 ID 或可编辑 Binding 时才继续使用显式 `lookinInspectable` modifier。其他构建仍需让编译器解析宏,但展开结果是原始 body,不会调用运行时 Probe。 diff --git a/Src/Main/Server/Connection/LKS_ConnectionManager.m b/Src/Main/Server/Connection/LKS_ConnectionManager.m index e4011e0c..7e024212 100644 --- a/Src/Main/Server/Connection/LKS_ConnectionManager.m +++ b/Src/Main/Server/Connection/LKS_ConnectionManager.m @@ -16,11 +16,14 @@ #import "LookinServerDefines.h" #import "LKS_TraceManager.h" #import "LKS_MultiplatformAdapter.h" -#import "ECOChannelManager.h" - #if LOOKIN_SERVER_WIRELESS +#import "ECOChannelManager.h" +#if __has_include() +#import +#else @import CocoaAsyncSocket; #endif +#endif NSString *const LKS_ConnectionDidEndNotificationName = @"LKS_ConnectionDidEndNotificationName"; @@ -29,12 +32,14 @@ @interface LKS_ConnectionManager () @property(nonatomic, weak) Lookin_PTChannel *peerChannel_; @property(nonatomic, strong) LKS_RequestHandler *requestHandler; +#if LOOKIN_SERVER_WIRELESS @property(nonatomic, strong) LKS_RequestHandler *wirelessRequestHandler; @property(nonatomic, strong) ECOChannelManager *wirelessChannel; @property(nonatomic, strong) ECOChannelDeviceInfo *wirelessDevice; @property BOOL hasStartWirelessConnnection; +#endif @end @@ -79,7 +84,9 @@ - (instancetype)init { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleGetLookinInfo:) name:@"GetLookinInfo" object:nil]; self.requestHandler = [LKS_RequestHandler new]; +#if LOOKIN_SERVER_WIRELESS self.wirelessRequestHandler = LKS_RequestHandler.wireless; +#endif } return self; } @@ -97,9 +104,12 @@ - (void)startWirelessConnection { __weak __typeof(self) weakSelf = self; // 接收到数据回调 self.wirelessChannel.receivedBlock = ^(ECOChannelDeviceInfo *device, NSData *data, NSDictionary *extraInfo) { - NSLog(@"🚀 Lookin receivedBlock device:%@", device); NSNumber *type = extraInfo[@"type"]; NSNumber *tag = extraInfo[@"tag"]; + if (![type isKindOfClass:NSNumber.class] || ![tag isKindOfClass:NSNumber.class] || + ![weakSelf.wirelessRequestHandler canHandleRequestType:type.unsignedIntValue]) { + return; + } id object = nil; id unarchivedObject = [NSKeyedUnarchiver unarchiveObjectWithData:data]; if ([unarchivedObject isKindOfClass:[LookinConnectionAttachment class]]) { @@ -114,32 +124,29 @@ - (void)startWirelessConnection { }; // 设备连接变更 self.wirelessChannel.deviceBlock = ^(ECOChannelDeviceInfo *device, BOOL isConnected) { - NSLog(@"🚀 Lookin deviceBlock device:%@", device); if ([device isEqual:weakSelf.wirelessDevice] && !isConnected) { weakSelf.wirelessDevice = nil; } }; // 授权状态变更回调 self.wirelessChannel.authStateChangedBlock = ^(ECOChannelDeviceInfo *device, ECOAuthorizeResponseType authState) { - NSLog(@"🚀 Lookin authStateChangedBlock device:%@ authState:%ld", device, authState); if (authState == ECOAuthorizeResponseType_AllowAlways) { weakSelf.wirelessDevice = device; } }; // 请求授权状态认证回调 self.wirelessChannel.requestAuthBlock = ^(ECOChannelDeviceInfo *device, ECOAuthorizeResponseType authState) { - NSLog(@"🚀 Lookin requestAuthBlock device:%@ authState:%ld", device, authState); - NSString *title = @"Lookin 连接请求"; - NSString *message = [NSString stringWithFormat:@"%@ 的Lookin想要连接你的设备,如果你想启用调试功能,请选择允许", device.hostName ?: device.ipAddress]; + NSString *title = @"Lookin Connection Request"; + NSString *message = [NSString stringWithFormat:@"Lookin on %@ wants to inspect this app over your local network. Only allow a computer you trust.", device.hostName.length ? device.hostName : @"a Mac"]; UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; - UIAlertAction *denyAction = [UIAlertAction actionWithTitle:@"拒绝" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { + UIAlertAction *denyAction = [UIAlertAction actionWithTitle:@"Deny" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { [weakSelf.wirelessChannel sendAuthorizationMessageToDevice:device state:ECOAuthorizeResponseType_Deny showAuthAlert:NO]; }]; - UIAlertAction *allowOnceAction = [UIAlertAction actionWithTitle:@"允许一次" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { + UIAlertAction *allowOnceAction = [UIAlertAction actionWithTitle:@"Allow Once" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { [weakSelf.wirelessChannel sendAuthorizationMessageToDevice:device state:ECOAuthorizeResponseType_AllowOnce showAuthAlert:NO]; weakSelf.wirelessDevice = device; }]; - UIAlertAction *allowAlwaysAction = [UIAlertAction actionWithTitle:@"始终允许" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { + UIAlertAction *allowAlwaysAction = [UIAlertAction actionWithTitle:@"Always Allow" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { [weakSelf.wirelessChannel sendAuthorizationMessageToDevice:device state:ECOAuthorizeResponseType_AllowAlways showAuthAlert:NO]; weakSelf.wirelessDevice = device; }]; @@ -148,7 +155,7 @@ - (void)startWirelessConnection { [alertController addAction:allowAlwaysAction]; dispatch_async(dispatch_get_main_queue(), ^{ - UIViewController *rootVC = [UIApplication sharedApplication].keyWindow.rootViewController; + UIViewController *rootVC = [LKS_MultiplatformAdapter keyWindow].rootViewController; [rootVC presentViewController:alertController animated:YES completion:nil]; }); }; @@ -281,9 +288,11 @@ - (void)pushData:(NSObject *)data type:(uint32_t)type isWireless:(BOOL)isWireles - (void)_sendData:(NSObject *)data frameOfType:(uint32_t)frameOfType tag:(uint32_t)tag isWireless:(BOOL)isWireless { NSData *archivedData = [NSKeyedArchiver archivedDataWithRootObject:data]; if (isWireless) { +#if LOOKIN_SERVER_WIRELESS if (self.wirelessDevice.isConnected) { [self.wirelessChannel sendPacket:archivedData extraInfo:@{@"tag": @(tag), @"type": @(frameOfType)} toDevice:self.wirelessDevice]; } +#endif } else { if (self.peerChannel_) { dispatch_data_t payload = [archivedData createReferencingDispatchData]; diff --git a/Src/Main/Shared/Channel/Bonjour/ECONetServiceBrowser.m b/Src/Main/Shared/Channel/Bonjour/ECONetServiceBrowser.m index 983615d0..f70f18d1 100644 --- a/Src/Main/Shared/Channel/Bonjour/ECONetServiceBrowser.m +++ b/Src/Main/Shared/Channel/Bonjour/ECONetServiceBrowser.m @@ -77,19 +77,20 @@ - (void)netServiceBrowser:(NSNetServiceBrowser *)browser didNotSearch:(NSDiction if (@available(iOS 14.0, *)) { NSNetServicesError errorCode = [errorDict[@"NSNetServicesErrorCode"] integerValue]; if (errorCode == -72008) { - //iOS14新增本地网络隐私权限,提示用户如何设置并忽略 + // Explain the required local-network declarations and stop retrying. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - NSString *title = @"Echo 连接提示"; - NSString *message = @"由于iOS14本地网络权限限制,请在Info.plist中设置NSLocalNetworkUsageDescription和NSBonjourServices,详细内容见:https://github.com/didi/echo"; + NSString *title = @"Local Network Access Required"; + NSString *message = @"Wireless inspection requires local-network permission plus NSLocalNetworkUsageDescription and _Lookin._tcp in NSBonjourServices."; UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; - UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"好的" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { + UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { }]; [alertController addAction:confirmAction]; - UIViewController *rootVC = [UIApplication sharedApplication].keyWindow.rootViewController; + UIWindow *window = UIApplication.sharedApplication.windows.firstObject; + UIViewController *rootVC = window.rootViewController; [rootVC presentViewController:alertController animated:YES completion:nil]; }); - NSLog(@">>Echo Warning:Bonjour服务错误,由于iOS14本地网络权限限制,请在Info.plist中设置NSLocalNetworkUsageDescription和NSBonjourServices,详细内容见:https://github.com/didi/echo"); + NSLog(@"Lookin wireless inspection needs local-network permission and Bonjour declarations."); return; } } diff --git a/Src/Main/Shared/Channel/ECOChannelAppInfo.m b/Src/Main/Shared/Channel/ECOChannelAppInfo.m index df68ddfa..99b7364c 100644 --- a/Src/Main/Shared/Channel/ECOChannelAppInfo.m +++ b/Src/Main/Shared/Channel/ECOChannelAppInfo.m @@ -8,6 +8,10 @@ #import "ECOChannelAppInfo.h" +#if TARGET_OS_IPHONE +@import UIKit; +#endif + static NSString *_ecoUniqueAppId = nil; static NSString *_ecoUniqueAppName = nil; diff --git a/Src/Main/Shared/Channel/ECOChannelDeviceInfo.m b/Src/Main/Shared/Channel/ECOChannelDeviceInfo.m index f2a63c9e..e40ac106 100644 --- a/Src/Main/Shared/Channel/ECOChannelDeviceInfo.m +++ b/Src/Main/Shared/Channel/ECOChannelDeviceInfo.m @@ -12,8 +12,15 @@ #include #include -static NSInteger const ECOINET_ADDRSTRLEN = 16; -static NSInteger const ECOINET6_ADDRSTRLEN = 46; +#if TARGET_OS_IPHONE +@import UIKit; +#endif + +enum { + ECOINET_ADDRSTRLEN = 16, + ECOINET6_ADDRSTRLEN = 46, +}; +static NSString *const ECOMacDeviceIdentifierKey = @"LookinWirelessMacDeviceIdentifier"; static NSString *_macUUIDString = nil; @interface ECOChannelDeviceInfo() @@ -89,7 +96,12 @@ - (instancetype)init { - (void)setupMacDeviceInfo { //uuid if (!_macUUIDString) { - _macUUIDString = [[NSUUID UUID] UUIDString]; + NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults; + _macUUIDString = [defaults stringForKey:ECOMacDeviceIdentifierKey]; + if (!_macUUIDString.length) { + _macUUIDString = NSUUID.UUID.UUIDString; + [defaults setObject:_macUUIDString forKey:ECOMacDeviceIdentifierKey]; + } } self.uuid = _macUUIDString; //设备 @@ -136,7 +148,7 @@ - (NSDictionary *)getIPAddresses { continue; // deeply nested code harder to read } const struct sockaddr_in *addr = (const struct sockaddr_in*)interface->ifa_addr; - char addrBuf[ MAX(ECOINET_ADDRSTRLEN, ECOINET6_ADDRSTRLEN) ]; + char addrBuf[ECOINET6_ADDRSTRLEN]; if(addr && (addr->sin_family==AF_INET || addr->sin_family==AF_INET6)) { NSString *name = [NSString stringWithUTF8String:interface->ifa_name]; NSString *type; @@ -206,4 +218,8 @@ - (BOOL)isEqual:(id)object { return NO; } +- (NSUInteger)hash { + return self.uuid.hash ^ self.appInfo.appId.hash; +} + @end diff --git a/Src/Main/Shared/Channel/ECOSocketChannel.m b/Src/Main/Shared/Channel/ECOSocketChannel.m index da957ec5..2d8e812e 100644 --- a/Src/Main/Shared/Channel/ECOSocketChannel.m +++ b/Src/Main/Shared/Channel/ECOSocketChannel.m @@ -7,7 +7,6 @@ // #import "ECOSocketChannel.h" -#import #import #include @@ -16,10 +15,18 @@ #import "LookinDefines.h" +#if __has_include() +#import +#else +@import CocoaAsyncSocket; +#endif + //static uint16_t const ECOClientSockeListenPortNumber = 23235; //static uint16_t const ECOSocketAcceptPortNumber = 23234; static CGFloat const ECOSocketRetryListenDelay = 1.f; static NSInteger const ECOSocketHeadOffsetValue = 10; +static NSUInteger const ECOSocketMaximumHeaderLength = 16 * 1024; +static NSUInteger const ECOSocketMaximumPayloadLength = 64 * 1024 * 1024; static NSString *const ECHOAuthorizedDevicesKey = @"echoAuthorizedDevicesKey"; @interface ECOSocketChannel() @@ -235,6 +242,9 @@ - (void)sendPacket:(NSData *)packet - (void)sendAuthorizationMessageToDevice:(ECOChannelDeviceInfo *)device state:(ECOAuthorizeResponseType)responseType showAuthAlert:(BOOL)showAuthAlert { + if (responseType < ECOAuthorizeResponseType_Deny || responseType > ECOAuthorizeResponseType_AllowAlways) { + return; + } //修改数据 ECOChannelDeviceInfo *authDevice = device; device.showAuthAlert = showAuthAlert; @@ -285,6 +295,18 @@ - (void)sendAuthorizationMessageToDevice:(ECOChannelDeviceInfo *)device } } #else + NSString *identifier = [self trustedIdentifierForDevice:device]; + if (identifier.length) { + if (responseType == ECOAuthorizeResponseType_AllowAlways) { + if (![self.whitelistDevices containsObject:identifier]) { + [self.whitelistDevices addObject:identifier]; + [self saveWhiteListDevices]; + } + } else if ([self.whitelistDevices containsObject:identifier]) { + [self.whitelistDevices removeObject:identifier]; + [self saveWhiteListDevices]; + } + } if ([self.delegate respondsToSelector:@selector(channel:device:didChangedAuthState:)]) { [self.delegate channel:self device:device didChangedAuthState:responseType]; } @@ -336,7 +358,7 @@ - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(ui // NSLog(@"%s",__func__); #if TARGET_OS_IPHONE //读取数据 - [sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:ECOSocketHeadTag_Device]; + [self readHeaderFromSocket:sock tag:ECOSocketHeadTag_Device]; #endif } @@ -346,14 +368,14 @@ - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSo [self p_lock]; [self.sockets addObject:newSocket]; newSocket.delegate = self; - [newSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:ECOSocketHeadTag_Device]; + [self readHeaderFromSocket:newSocket tag:ECOSocketHeadTag_Device]; [self p_unlock]; //发送设备信息 [self sendDeviceInfo:newSocket hostName:nil]; #else [self.clientSockets addObject:newSocket]; newSocket.delegate = self; - [newSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:ECOSocketHeadTag_ClientListen]; + [self readHeaderFromSocket:newSocket tag:ECOSocketHeadTag_ClientListen]; #endif } @@ -362,28 +384,54 @@ - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)t if (tag == ECOSocketHeadTag_Data || tag == ECOSocketHeadTag_Device || tag == ECOSocketHeadTag_ClientListen) { - NSData *headerData = [data subdataWithRange:NSMakeRange(0, data.length - [GCDAsyncSocket CRLFData].length)]; - NSDictionary *header = [NSJSONSerialization JSONObjectWithData:headerData options:NSJSONReadingAllowFragments error:nil]; + NSUInteger delimiterLength = GCDAsyncSocket.CRLFData.length; + if (data.length <= delimiterLength) { + [sock disconnect]; + return; + } + NSData *headerData = [data subdataWithRange:NSMakeRange(0, data.length - delimiterLength)]; + id headerObject = [NSJSONSerialization JSONObjectWithData:headerData options:0 error:nil]; + if (![headerObject isKindOfClass:NSDictionary.class]) { + [sock disconnect]; + return; + } + NSDictionary *header = headerObject; NSInteger version = [header[@"version"] integerValue]; - if (version < ECOSocketProtocolVersion) { + if (version != ECOSocketProtocolVersion) { + [sock disconnect]; return; } NSInteger length = [header[@"len"] integerValue]; NSInteger mainType = [header[@"mType"] integerValue]; + if (length <= 0 || length > ECOSocketMaximumPayloadLength) { + [sock disconnect]; + return; + } if (mainType == ECOHeadMainType_Authorization) { [sock readDataToLength:length withTimeout:-1 tag:ECOSocketDataTag_Authorization]; }else if(mainType == ECOHeadMainType_Data) { ECOChannelDeviceInfo *deviceInfo = sock.userData; - NSDictionary *extraInfo = header[@"extra"]; + if (tag == ECOSocketHeadTag_Data && !deviceInfo) { + [sock disconnect]; + return; + } + id extraObject = header[@"extra"]; + NSDictionary *extraInfo = [extraObject isKindOfClass:NSDictionary.class] ? extraObject : nil; deviceInfo.extraData = extraInfo; [sock readDataToLength:length withTimeout:-1 tag:tag + ECOSocketHeadOffsetValue]; }else if (mainType == ECOHeadMainType_ClientListen) { [sock readDataToLength:length withTimeout:-1 tag:tag + ECOSocketHeadOffsetValue]; + } else { + [sock disconnect]; } return; } if (tag == ECOSocketDataTag_Device) { ECOChannelDeviceInfo *deviceInfo = [[ECOChannelDeviceInfo alloc] initWithData:data]; + if (![self hasValidIdentity:deviceInfo]) { + [sock disconnect]; + return; + } sock.userData = deviceInfo; #if TARGET_OS_OSX NSString *uniId = [NSString stringWithFormat:@"%@_%@",deviceInfo.uuid, deviceInfo.appInfo.appId]; @@ -401,6 +449,10 @@ - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)t } if (tag == ECOSocketDataTag_ClientListen) { ECOChannelDeviceInfo *deviceInfo = [[ECOChannelDeviceInfo alloc] initWithData:data]; + if (![self hasValidIdentity:deviceInfo]) { + [sock disconnect]; + return; + } BOOL isConnected = [self isEchoConnectedOfDevice:deviceInfo]; if (!isConnected) { [self connectToIPAddress:deviceInfo.ipAddress]; @@ -414,10 +466,17 @@ - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)t //授权信息 ECOChannelDeviceInfo *deviceInfo = sock.userData; ECOChannelDeviceInfo *tempDevice = [[ECOChannelDeviceInfo alloc] initWithData:data]; + if (![self hasValidIdentity:deviceInfo] || ![self hasValidIdentity:tempDevice] || + tempDevice.authorizedType < ECOAuthorizeResponseType_Deny || + tempDevice.authorizedType > ECOAuthorizeResponseType_AllowAlways) { + [sock disconnect]; + return; + } BOOL isAlwaysAllow = tempDevice.authorizedType == ECOAuthorizeResponseType_AllowAlways; #if TARGET_OS_IPHONE deviceInfo.hostName = tempDevice.hostName; - if (tempDevice.showAuthAlert) { + BOOL canReconnectSilently = isAlwaysAllow && [self isPersistentlyTrustedDevice:deviceInfo]; + if (tempDevice.showAuthAlert || !canReconnectSilently) { //弹窗提示用户 if ([self.delegate respondsToSelector:@selector(channel:device:willRequestAuthState:)]) { [self.delegate channel:self device:deviceInfo willRequestAuthState:tempDevice.authorizedType]; @@ -463,7 +522,7 @@ - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)t } } //读取下次发送的数据 - [sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:ECOSocketHeadTag_Data]; + [self readHeaderFromSocket:sock tag:ECOSocketHeadTag_Data]; } - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err { // NSLog(@"%s",__func__); @@ -487,6 +546,29 @@ - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError * } } #pragma mark - helper +- (void)readHeaderFromSocket:(GCDAsyncSocket *)socket tag:(long)tag { + [socket readDataToData:GCDAsyncSocket.CRLFData + withTimeout:-1 + maxLength:ECOSocketMaximumHeaderLength + tag:tag]; +} + +- (NSString *)trustedIdentifierForDevice:(ECOChannelDeviceInfo *)device { + if (!device.uuid.length || !device.appInfo.appId.length) { + return nil; + } + return [NSString stringWithFormat:@"%@_%@", device.uuid, device.appInfo.appId]; +} + +- (BOOL)hasValidIdentity:(ECOChannelDeviceInfo *)device { + return device.uuid.length > 0 && device.appInfo.appId.length > 0; +} + +- (BOOL)isPersistentlyTrustedDevice:(ECOChannelDeviceInfo *)device { + NSString *identifier = [self trustedIdentifierForDevice:device]; + return identifier.length && [self.whitelistDevices containsObject:identifier]; +} + - (void)p_lock { [_socketLock lock]; } diff --git a/UPSTREAM_PATCHES.md b/UPSTREAM_PATCHES.md index 870dbb79..ec2b721b 100644 --- a/UPSTREAM_PATCHES.md +++ b/UPSTREAM_PATCHES.md @@ -7,5 +7,6 @@ This fork incorporates selected community pull requests that were not merged by | `ee11b92` | [QMUI/LookinServer#159](https://github.com/QMUI/LookinServer/pull/159) | `6db2bb1858865255f7fa6c5424d08b509a4b9190` | None. Renames the export overlay property to avoid the iOS 18 `maskView` collision. | | `d9f34d9` | [QMUI/LookinServer#165](https://github.com/QMUI/LookinServer/pull/165) | `2377dc6cb63be48bc6aef825110e35e8a86bb9ec` | Uses an early return when running inside SwiftUI Previews. | | `7929384` | [QMUI/LookinServer#173](https://github.com/QMUI/LookinServer/pull/173) | `cb1f9baeda8c294c758a0a131a63283efe103e8a` | Treats a null dispatch payload as zero bytes so the existing completion and error paths still run. | +| `codex/upstream-wireless` | [QMUI/LookinServer#164](https://github.com/QMUI/LookinServer/pull/164) | `4f63587`, `f33b8b4`, `6000fe1`, `6f526dd`, `1d7a5ca`, `8dabc32`, `3266602` | Paired with `nova286/Lookin#42`; preserves opt-in CocoaPods packaging, excludes wireless from the default SPM product, requires device-side confirmation, persists peer identities on both sides, bounds protocol frames, and keeps runtime UI text in English. Merge commits and the upstream default-SPM CocoaAsyncSocket dependency were intentionally excluded. | Large or cross-repository features are ported on dedicated branches instead of being applied directly. Every imported patch must build against the fork's current default branch before it is merged. From 42c2510f81259760d484042815854c246924aabe Mon Sep 17 00:00:00 2001 From: hongbo <1049145827@qq.com> Date: Sat, 18 Jul 2026 16:53:25 +0800 Subject: [PATCH 09/13] Avoid logging wireless device identifiers --- Src/Main/Shared/Channel/ECOChannelManager.m | 2 -- Src/Main/Shared/Channel/ECOSocketChannel.m | 2 -- Src/Main/Shared/Channel/ECOUSBChannel.m | 5 ----- 3 files changed, 9 deletions(-) diff --git a/Src/Main/Shared/Channel/ECOChannelManager.m b/Src/Main/Shared/Channel/ECOChannelManager.m index d6ac3eff..9bb31a92 100644 --- a/Src/Main/Shared/Channel/ECOChannelManager.m +++ b/Src/Main/Shared/Channel/ECOChannelManager.m @@ -78,7 +78,6 @@ - (BOOL)isConnected { } #pragma mark - ECOChannelConnectedDeviceProtocol methods - (void)channel:(ECOBaseChannel *)channel didConnectedToDevice:(ECOChannelDeviceInfo *)device { - NSLog(@">> [ECOChannelManager] did Connected to device:%@", [device description]); // //状态回调 // if (device.authorizedType != ECOAuthorizeResponseType_Deny) { // !self.connectBlock ?: self.connectBlock([self isConnected]); @@ -87,7 +86,6 @@ - (void)channel:(ECOBaseChannel *)channel didConnectedToDevice:(ECOChannelDevice !self.deviceBlock ?: self.deviceBlock(device, YES); } - (void)channel:(ECOBaseChannel *)channel didDisconnectWithDevice:(ECOChannelDeviceInfo *)device { - NSLog(@">> [ECOChannelManager] did Disconnect to device:%@", [device description]); // //状态回调 // if (device.authorizedType != ECOAuthorizeResponseType_Deny) { // !self.connectBlock ?: self.connectBlock([self isConnected]); diff --git a/Src/Main/Shared/Channel/ECOSocketChannel.m b/Src/Main/Shared/Channel/ECOSocketChannel.m index 2d8e812e..a6d0ae30 100644 --- a/Src/Main/Shared/Channel/ECOSocketChannel.m +++ b/Src/Main/Shared/Channel/ECOSocketChannel.m @@ -456,8 +456,6 @@ - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)t BOOL isConnected = [self isEchoConnectedOfDevice:deviceInfo]; if (!isConnected) { [self connectToIPAddress:deviceInfo.ipAddress]; - }else{ - NSLog(@"当前Echo主机已连接:%@",deviceInfo.ipAddress); } [sock setDelegate:nil]; [self.clientSockets removeObject:sock]; diff --git a/Src/Main/Shared/Channel/ECOUSBChannel.m b/Src/Main/Shared/Channel/ECOUSBChannel.m index a1d17252..5a58fbfe 100644 --- a/Src/Main/Shared/Channel/ECOUSBChannel.m +++ b/Src/Main/Shared/Channel/ECOUSBChannel.m @@ -88,7 +88,6 @@ - (void)startListeningForDevices { } - (void)onUSBDeviceDidAttach:(NSNotification *)note { NSNumber *deviceID = [note.userInfo objectForKey:@"DeviceID"]; - NSLog(@"<< [ECOUSBChannel] PTUSBDeviceDidAttachNotification:%@", deviceID); // [self showAlertWithMessage:[NSString stringWithFormat:@"usb设备连接:%@",deviceID]]; dispatch_async(_notConnectedQueue, ^{ @@ -103,7 +102,6 @@ - (void)onUSBDeviceDidAttach:(NSNotification *)note { } - (void)onUSBDeviceDidDetach:(NSNotification *)note { NSNumber *deviceID = [note.userInfo objectForKey:@"DeviceID"]; - NSLog(@"<< [ECOUSBChannel] PTUSBDeviceDidDetachNotification:%@", deviceID); // [self showAlertWithMessage:[NSString stringWithFormat:@"usb设备断开:%@",deviceID]]; if ([self.connectingToDeviceID isEqualToNumber:deviceID]) { @@ -138,7 +136,6 @@ - (void)connectToLocalIPv4Port { [self disconnectFromCurrentChannel]; self.connectedChannel = channel; channel.userInfo = address; - NSLog(@"<< [ECOUSBChannel] Connected to %@", address); } }]; } @@ -169,7 +166,6 @@ - (void)connectToUSBDevice { }else{ self.connectedDeviceID = self.connectingToDeviceID; self.connectedChannel = channel; - NSLog(@"<< [ECOUSBChannel] Connect to device #%@\n%@", channel.userInfo, self.connectedDeviceProperties); //发送ping信息 [self ping]; } @@ -184,7 +180,6 @@ - (void)disconnectFromCurrentChannel { } - (void)didDisconnectFromDevice:(NSNumber*)deviceID { - NSLog(@"<< [ECOUSBChannel] Disconnected from device:%@", deviceID); if ([self.connectedDeviceID isEqualToNumber:deviceID]) { [self willChangeValueForKey:@"connectedDeviceID"]; self.connectedDeviceID = nil; From d195c9295c1c8f42f4af5f13d3b66e4b284a695d Mon Sep 17 00:00:00 2001 From: hongbo <1049145827@qq.com> Date: Sat, 18 Jul 2026 16:58:17 +0800 Subject: [PATCH 10/13] Make wireless shared pod self-contained --- LookinShared.podspec | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/LookinShared.podspec b/LookinShared.podspec index 05bcad98..226a18cf 100644 --- a/LookinShared.podspec +++ b/LookinShared.podspec @@ -25,7 +25,13 @@ Pod::Spec.new do |spec| } spec.subspec 'Wireless' do |ss| - ss.source_files = 'Src/Main/Shared/Channel/**/*' + # The channel implementation imports shared protocol and Peertalk headers. + # A consumer may select this subspec directly (the macOS client does), so it + # must be self-contained instead of relying on LookinServer/Core. + ss.source_files = [ + 'Src/Main/Shared/**/*', + 'Src/Base/**/*' + ] ss.dependency 'CocoaAsyncSocket' ss.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) SHOULD_COMPILE_LOOKIN_SERVER=1 LOOKIN_SERVER_WIRELESS=1' From 76f0edd43c03df19b217fd0eee05b806e104fa6a Mon Sep 17 00:00:00 2001 From: hongbo <1049145827@qq.com> Date: Sat, 18 Jul 2026 17:01:01 +0800 Subject: [PATCH 11/13] Fix wireless channel documentation warning --- Src/Main/Shared/Channel/ECOChannelManager.h | 1 - 1 file changed, 1 deletion(-) diff --git a/Src/Main/Shared/Channel/ECOChannelManager.h b/Src/Main/Shared/Channel/ECOChannelManager.h index fe886be9..d5609a09 100644 --- a/Src/Main/Shared/Channel/ECOChannelManager.h +++ b/Src/Main/Shared/Channel/ECOChannelManager.h @@ -42,7 +42,6 @@ typedef void(^ECOChannelRequestAuthStateBlock)(ECOChannelDeviceInfo *device, ECO 发送数据包 @param packet 数据包 - @param type 数据包类型,json或者普通数据包 @param extraInfo 透传信息 @param device 要接收消息的设备,如果传入nil,则对所有已授权连接的设备发送消息 */ From 01460f6d6b857893afde77f7bbe2ca7adb70c012 Mon Sep 17 00:00:00 2001 From: hongbo <1049145827@qq.com> Date: Sat, 18 Jul 2026 17:06:09 +0800 Subject: [PATCH 12/13] Harden wireless session lifecycle --- README.md | 8 +- .../Server/Connection/LKS_ConnectionManager.m | 82 ++++++++++--------- .../Channel/Bonjour/ECONetServiceBrowser.h | 1 + .../Channel/Bonjour/ECONetServiceBrowser.m | 7 ++ .../Channel/Bonjour/ECONetServicePublisher.h | 1 + .../Channel/Bonjour/ECONetServicePublisher.m | 5 ++ Src/Main/Shared/Channel/ECOChannelManager.h | 3 + Src/Main/Shared/Channel/ECOChannelManager.m | 4 + Src/Main/Shared/Channel/ECOSocketChannel.h | 2 + Src/Main/Shared/Channel/ECOSocketChannel.m | 31 ++++++- Src/Main/Shared/Channel/ECOUSBChannel.h | 2 + Src/Main/Shared/Channel/ECOUSBChannel.m | 17 +++- UPSTREAM_PATCHES.md | 2 +- 13 files changed, 117 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index a0c080a4..6ae4a4ad 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ To use Lookin macOS app, you need to integrate LookinServer (iOS Framework of Lo ## Experimental wireless connection -Wireless inspection is opt-in, CocoaPods-only, and intended for Debug builds on a trusted local network. The transport is not encrypted. The iOS app asks for confirmation before accepting a new Mac, and remembered devices are validated independently on both sides. +Wireless inspection is opt-in, CocoaPods-only, and intended for Debug builds on a trusted local network. The transport is not encrypted or cryptographically authenticated. The iOS app asks for confirmation before accepting a new Mac, and remembered device identifiers are checked independently on both sides. One Mac may inspect an app over wireless at a time. ```ruby # Swift + wireless inspection @@ -60,7 +60,7 @@ NotificationCenter.default.post( #endif ``` -Post `Lookin_endWirelessConnection` to stop it. The default `Core`/`Swift` subspecs and the default Swift Package Manager product do not compile the wireless transport or CocoaAsyncSocket. +Post `Lookin_endWirelessConnection` to stop discovery, close active wireless transports, and forget the current session. The default `Core`/`Swift` subspecs and the default Swift Package Manager product do not compile the wireless transport or CocoaAsyncSocket. ## Experimental SwiftUI semantic hierarchy @@ -145,11 +145,11 @@ Lookin 可以查看与修改 iOS App 里的 UI 对象,类似于 Xcode 自带 ## 实验性无线连接 -无线检查能力是显式可选的,目前仅支持 CocoaPods,且只应在可信局域网的 Debug 构建中启用。传输内容没有加密;首次连接新 Mac 时 iOS App 会要求用户确认,“始终允许”会在两端分别校验已记住的设备身份。 +无线检查能力是显式可选的,目前仅支持 CocoaPods,且只应在可信局域网的 Debug 构建中启用。传输内容没有加密,设备标识也不是密码学身份证明;首次连接新 Mac 时 iOS App 会要求用户确认,“始终允许”会在两端分别校验已记住的设备标识。同一时刻仅允许一台 Mac 通过无线方式检查当前 App。 Swift 项目使用 `SwiftAndWireless` subspec,Objective-C 项目使用 `Wireless` subspec,并继续通过 `:configurations => ['Debug']` 限制构建配置。示例见上方英文文档。 -被检查 App 的 `Info.plist` 必须声明 `NSLocalNetworkUsageDescription`,并在 `NSBonjourServices` 中加入 `_Lookin._tcp`。App 启动并进入 active 状态后,发送 `Lookin_startWirelessConnection` 通知才会开始发现设备;发送 `Lookin_endWirelessConnection` 可停止。该能力不会自动启动,默认 `Core`/`Swift` subspec 与默认 SPM product 都不会编译无线通道或引入 CocoaAsyncSocket。 +被检查 App 的 `Info.plist` 必须声明 `NSLocalNetworkUsageDescription`,并在 `NSBonjourServices` 中加入 `_Lookin._tcp`。App 启动并进入 active 状态后,发送 `Lookin_startWirelessConnection` 通知才会开始发现设备;发送 `Lookin_endWirelessConnection` 会停止发现、关闭当前无线通道并清理本次会话。该能力不会自动启动,默认 `Core`/`Swift` subspec 与默认 SPM product 都不会编译无线通道或引入 CocoaAsyncSocket。 ## 实验性 SwiftUI 语义层级 diff --git a/Src/Main/Server/Connection/LKS_ConnectionManager.m b/Src/Main/Server/Connection/LKS_ConnectionManager.m index 7e024212..8af253db 100644 --- a/Src/Main/Server/Connection/LKS_ConnectionManager.m +++ b/Src/Main/Server/Connection/LKS_ConnectionManager.m @@ -110,51 +110,61 @@ - (void)startWirelessConnection { ![weakSelf.wirelessRequestHandler canHandleRequestType:type.unsignedIntValue]) { return; } - id object = nil; - id unarchivedObject = [NSKeyedUnarchiver unarchiveObjectWithData:data]; - if ([unarchivedObject isKindOfClass:[LookinConnectionAttachment class]]) { - LookinConnectionAttachment *attachment = (LookinConnectionAttachment *)unarchivedObject; - object = attachment.data; - } else { - object = unarchivedObject; - } dispatch_async(dispatch_get_main_queue(), ^{ + if (![device isEqual:weakSelf.wirelessDevice]) { + return; + } + id object = nil; + id unarchivedObject = [NSKeyedUnarchiver unarchiveObjectWithData:data]; + if ([unarchivedObject isKindOfClass:[LookinConnectionAttachment class]]) { + object = ((LookinConnectionAttachment *)unarchivedObject).data; + } else { + object = unarchivedObject; + } [weakSelf.wirelessRequestHandler handleRequestType:type.intValue tag:tag.intValue object:object]; }); }; // 设备连接变更 self.wirelessChannel.deviceBlock = ^(ECOChannelDeviceInfo *device, BOOL isConnected) { - if ([device isEqual:weakSelf.wirelessDevice] && !isConnected) { - weakSelf.wirelessDevice = nil; - } + dispatch_async(dispatch_get_main_queue(), ^{ + if ([device isEqual:weakSelf.wirelessDevice] && !isConnected) { + weakSelf.wirelessDevice = nil; + } + }); }; // 授权状态变更回调 self.wirelessChannel.authStateChangedBlock = ^(ECOChannelDeviceInfo *device, ECOAuthorizeResponseType authState) { - if (authState == ECOAuthorizeResponseType_AllowAlways) { - weakSelf.wirelessDevice = device; - } + dispatch_async(dispatch_get_main_queue(), ^{ + if (authState != ECOAuthorizeResponseType_AllowAlways) { + return; + } + if (!weakSelf.wirelessDevice.isConnected || [device isEqual:weakSelf.wirelessDevice]) { + weakSelf.wirelessDevice = device; + } else { + [weakSelf.wirelessChannel sendAuthorizationMessageToDevice:device state:ECOAuthorizeResponseType_Deny showAuthAlert:NO]; + } + }); }; // 请求授权状态认证回调 self.wirelessChannel.requestAuthBlock = ^(ECOChannelDeviceInfo *device, ECOAuthorizeResponseType authState) { - NSString *title = @"Lookin Connection Request"; - NSString *message = [NSString stringWithFormat:@"Lookin on %@ wants to inspect this app over your local network. Only allow a computer you trust.", device.hostName.length ? device.hostName : @"a Mac"]; - UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; - UIAlertAction *denyAction = [UIAlertAction actionWithTitle:@"Deny" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { - [weakSelf.wirelessChannel sendAuthorizationMessageToDevice:device state:ECOAuthorizeResponseType_Deny showAuthAlert:NO]; - }]; - UIAlertAction *allowOnceAction = [UIAlertAction actionWithTitle:@"Allow Once" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { - [weakSelf.wirelessChannel sendAuthorizationMessageToDevice:device state:ECOAuthorizeResponseType_AllowOnce showAuthAlert:NO]; - weakSelf.wirelessDevice = device; - }]; - UIAlertAction *allowAlwaysAction = [UIAlertAction actionWithTitle:@"Always Allow" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { - [weakSelf.wirelessChannel sendAuthorizationMessageToDevice:device state:ECOAuthorizeResponseType_AllowAlways showAuthAlert:NO]; - weakSelf.wirelessDevice = device; - }]; - [alertController addAction:denyAction]; - [alertController addAction:allowOnceAction]; - [alertController addAction:allowAlwaysAction]; - dispatch_async(dispatch_get_main_queue(), ^{ + if (weakSelf.wirelessDevice.isConnected && ![device isEqual:weakSelf.wirelessDevice]) { + [weakSelf.wirelessChannel sendAuthorizationMessageToDevice:device state:ECOAuthorizeResponseType_Deny showAuthAlert:NO]; + return; + } + NSString *message = [NSString stringWithFormat:@"Lookin on %@ wants to inspect this app over your local network. Only allow a computer you trust.", device.hostName.length ? device.hostName : @"a Mac"]; + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Lookin Connection Request" message:message preferredStyle:UIAlertControllerStyleAlert]; + [alertController addAction:[UIAlertAction actionWithTitle:@"Deny" style:UIAlertActionStyleCancel handler:^(__unused UIAlertAction *action) { + [weakSelf.wirelessChannel sendAuthorizationMessageToDevice:device state:ECOAuthorizeResponseType_Deny showAuthAlert:NO]; + }]]; + [alertController addAction:[UIAlertAction actionWithTitle:@"Allow Once" style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *action) { + weakSelf.wirelessDevice = device; + [weakSelf.wirelessChannel sendAuthorizationMessageToDevice:device state:ECOAuthorizeResponseType_AllowOnce showAuthAlert:NO]; + }]]; + [alertController addAction:[UIAlertAction actionWithTitle:@"Always Allow" style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *action) { + weakSelf.wirelessDevice = device; + [weakSelf.wirelessChannel sendAuthorizationMessageToDevice:device state:ECOAuthorizeResponseType_AllowAlways showAuthAlert:NO]; + }]]; UIViewController *rootVC = [LKS_MultiplatformAdapter keyWindow].rootViewController; [rootVC presentViewController:alertController animated:YES completion:nil]; }); @@ -165,13 +175,9 @@ - (void)startWirelessConnection { - (void)endWirelessConnection { self.hasStartWirelessConnnection = NO; - GCDAsyncSocket *asyncSocket = [self.wirelessChannel valueForKeyPath:@"socketChannel.cSocket"]; - if (asyncSocket) { - [asyncSocket setDelegate:nil]; - [asyncSocket disconnect]; - [self.wirelessChannel setValue:nil forKeyPath:@"socketChannel.cSocket"]; - } + [self.wirelessChannel stop]; self.wirelessChannel = nil; + self.wirelessDevice = nil; } #endif diff --git a/Src/Main/Shared/Channel/Bonjour/ECONetServiceBrowser.h b/Src/Main/Shared/Channel/Bonjour/ECONetServiceBrowser.h index f8f5fbd7..93316e70 100644 --- a/Src/Main/Shared/Channel/Bonjour/ECONetServiceBrowser.h +++ b/Src/Main/Shared/Channel/Bonjour/ECONetServiceBrowser.h @@ -15,5 +15,6 @@ typedef void(^ECONetServiceBrowserResolvedAddressesBlock)(NSArray *add @property (nonatomic, copy) ECONetServiceBrowserResolvedAddressesBlock addressesBlock; - (void)startBrowsing; +- (void)stopBrowsing; @end diff --git a/Src/Main/Shared/Channel/Bonjour/ECONetServiceBrowser.m b/Src/Main/Shared/Channel/Bonjour/ECONetServiceBrowser.m index f70f18d1..b131326f 100644 --- a/Src/Main/Shared/Channel/Bonjour/ECONetServiceBrowser.m +++ b/Src/Main/Shared/Channel/Bonjour/ECONetServiceBrowser.m @@ -40,6 +40,13 @@ - (void)startBrowsing { [self.serviceBrowser setDelegate:self]; [self.serviceBrowser searchForServicesOfType:LookinNetServiceType inDomain:LookinNetServiceDomain]; } +- (void)stopBrowsing { + [self.serviceBrowser stop]; + self.serviceBrowser.delegate = nil; + self.serviceBrowser = nil; + [self.services makeObjectsPerformSelector:@selector(stop)]; + [self.services removeAllObjects]; +} //重置查找服务 - (void)resetBrowserService { [self.serviceBrowser stop]; diff --git a/Src/Main/Shared/Channel/Bonjour/ECONetServicePublisher.h b/Src/Main/Shared/Channel/Bonjour/ECONetServicePublisher.h index d215e54d..b285621a 100644 --- a/Src/Main/Shared/Channel/Bonjour/ECONetServicePublisher.h +++ b/Src/Main/Shared/Channel/Bonjour/ECONetServicePublisher.h @@ -11,5 +11,6 @@ @interface ECONetServicePublisher : NSObject - (void)startPublish; +- (void)stopPublishing; @end diff --git a/Src/Main/Shared/Channel/Bonjour/ECONetServicePublisher.m b/Src/Main/Shared/Channel/Bonjour/ECONetServicePublisher.m index 0812bffd..e8acadd8 100644 --- a/Src/Main/Shared/Channel/Bonjour/ECONetServicePublisher.m +++ b/Src/Main/Shared/Channel/Bonjour/ECONetServicePublisher.m @@ -35,6 +35,11 @@ - (void)startPublish { self.netService.delegate = self; [self.netService publish]; } +- (void)stopPublishing { + [self.netService stop]; + self.netService.delegate = nil; + self.netService = nil; +} #pragma mark - NSNetServiceDelegate methods /* Sent to the NSNetService instance's delegate when the publication of the instance is complete and successful. */ diff --git a/Src/Main/Shared/Channel/ECOChannelManager.h b/Src/Main/Shared/Channel/ECOChannelManager.h index d5609a09..7375b58f 100644 --- a/Src/Main/Shared/Channel/ECOChannelManager.h +++ b/Src/Main/Shared/Channel/ECOChannelManager.h @@ -65,4 +65,7 @@ typedef void(^ECOChannelRequestAuthStateBlock)(ECOChannelDeviceInfo *device, ECO //链接IP地址的主机 - (void)connectToClientIPAddress:(NSString *)ipAddress; +/// Stop discovery/listening and close active transports. +- (void)stop; + @end diff --git a/Src/Main/Shared/Channel/ECOChannelManager.m b/Src/Main/Shared/Channel/ECOChannelManager.m index 9bb31a92..83d6fa75 100644 --- a/Src/Main/Shared/Channel/ECOChannelManager.m +++ b/Src/Main/Shared/Channel/ECOChannelManager.m @@ -41,6 +41,10 @@ - (instancetype)init - (void)connectToClientIPAddress:(NSString *)ipAddress { [self.socketChannel autoConnectToClientIPAddress:ipAddress]; } +- (void)stop { + [self.socketChannel stop]; + [self.ptChannel stop]; +} #pragma mark - 数据传输 //发送数据 - (void)sendPacket:(NSData *)packet diff --git a/Src/Main/Shared/Channel/ECOSocketChannel.h b/Src/Main/Shared/Channel/ECOSocketChannel.h index 6f12b74c..10fdc3b5 100644 --- a/Src/Main/Shared/Channel/ECOSocketChannel.h +++ b/Src/Main/Shared/Channel/ECOSocketChannel.h @@ -57,4 +57,6 @@ static const int ECOHeadSubType_Data = 1; state:(ECOAuthorizeResponseType)responseType showAuthAlert:(BOOL)showAuthAlert; +- (void)stop; + @end diff --git a/Src/Main/Shared/Channel/ECOSocketChannel.m b/Src/Main/Shared/Channel/ECOSocketChannel.m index a6d0ae30..23a14d0d 100644 --- a/Src/Main/Shared/Channel/ECOSocketChannel.m +++ b/Src/Main/Shared/Channel/ECOSocketChannel.m @@ -40,6 +40,7 @@ @interface ECOSocketChannel() //Client端主动连接的监听socket @property (nonatomic, strong) GCDAsyncSocket *cSocket; @property (nonatomic, strong) NSMutableArray *clientSockets; +@property (nonatomic, assign) BOOL stopped; @end @@ -106,6 +107,9 @@ - (void)setupClientListenSocket { } } - (void)startListening { + if (self.stopped) { + return; + } [self p_lock]; [self.sockets removeAllObjects]; [self p_unlock]; @@ -131,9 +135,34 @@ - (void)startListening { //重试监听 - (void)restartListening { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(ECOSocketRetryListenDelay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self startListening]; + if (!self.stopped) { + [self startListening]; + } }); } + +- (void)stop { + self.stopped = YES; + [self.browser stopBrowsing]; + [self.publisher stopPublishing]; + + [self.mSocket setDelegate:nil]; + [self.mSocket disconnect]; + self.mSocket = nil; + [self.cSocket setDelegate:nil]; + [self.cSocket disconnect]; + self.cSocket = nil; + + [self p_lock]; + NSArray *sockets = [self.sockets arrayByAddingObjectsFromArray:self.clientSockets]; + [self.sockets removeAllObjects]; + [self.clientSockets removeAllObjects]; + [self p_unlock]; + for (GCDAsyncSocket *socket in sockets) { + [socket setDelegate:nil]; + [socket disconnect]; + } +} #pragma mark - Socket连接 //Client侧连接Mac侧 - (void)connectToAddresses:(NSArray *)addresses diff --git a/Src/Main/Shared/Channel/ECOUSBChannel.h b/Src/Main/Shared/Channel/ECOUSBChannel.h index 15e72462..e3db75e5 100644 --- a/Src/Main/Shared/Channel/ECOUSBChannel.h +++ b/Src/Main/Shared/Channel/ECOUSBChannel.h @@ -26,4 +26,6 @@ typedef void(^ECOUSBChannelDidAttachBlock)(NSString *ipAddress); @property (nonatomic, copy) ECOUSBChannelDidAttachBlock attachBlock; +- (void)stop; + @end diff --git a/Src/Main/Shared/Channel/ECOUSBChannel.m b/Src/Main/Shared/Channel/ECOUSBChannel.m index 5a58fbfe..7f13e7ff 100644 --- a/Src/Main/Shared/Channel/ECOUSBChannel.m +++ b/Src/Main/Shared/Channel/ECOUSBChannel.m @@ -36,11 +36,20 @@ @implementation ECOUSBChannel #pragma mark - LifeCycle methods - (void)dealloc { - NSLog(@"%s",__func__); - if (self.serverChannel) { - [self.serverChannel close]; - } + [self stop]; +} + +- (void)stop { + [NSObject cancelPreviousPerformRequestsWithTarget:self]; [[NSNotificationCenter defaultCenter] removeObserver:self]; + [self.serverChannel close]; + [self.peerChannel close]; + [self.connectedChannel close]; + self.serverChannel = nil; + self.peerChannel = nil; + self.connectedChannel = nil; + self.connectingToDeviceID = nil; + self.connectedDeviceID = nil; } //初始化 - (void)setupChannel { diff --git a/UPSTREAM_PATCHES.md b/UPSTREAM_PATCHES.md index ec2b721b..b9120f69 100644 --- a/UPSTREAM_PATCHES.md +++ b/UPSTREAM_PATCHES.md @@ -7,6 +7,6 @@ This fork incorporates selected community pull requests that were not merged by | `ee11b92` | [QMUI/LookinServer#159](https://github.com/QMUI/LookinServer/pull/159) | `6db2bb1858865255f7fa6c5424d08b509a4b9190` | None. Renames the export overlay property to avoid the iOS 18 `maskView` collision. | | `d9f34d9` | [QMUI/LookinServer#165](https://github.com/QMUI/LookinServer/pull/165) | `2377dc6cb63be48bc6aef825110e35e8a86bb9ec` | Uses an early return when running inside SwiftUI Previews. | | `7929384` | [QMUI/LookinServer#173](https://github.com/QMUI/LookinServer/pull/173) | `cb1f9baeda8c294c758a0a131a63283efe103e8a` | Treats a null dispatch payload as zero bytes so the existing completion and error paths still run. | -| `codex/upstream-wireless` | [QMUI/LookinServer#164](https://github.com/QMUI/LookinServer/pull/164) | `4f63587`, `f33b8b4`, `6000fe1`, `6f526dd`, `1d7a5ca`, `8dabc32`, `3266602` | Paired with `nova286/Lookin#42`; preserves opt-in CocoaPods packaging, excludes wireless from the default SPM product, requires device-side confirmation, persists peer identities on both sides, bounds protocol frames, and keeps runtime UI text in English. Merge commits and the upstream default-SPM CocoaAsyncSocket dependency were intentionally excluded. | +| `codex/upstream-wireless` | [QMUI/LookinServer#164](https://github.com/QMUI/LookinServer/pull/164) | `4f63587`, `f33b8b4`, `6000fe1`, `6f526dd`, `1d7a5ca`, `8dabc32`, `3266602` | Paired with `nova286/Lookin#42`; preserves opt-in CocoaPods packaging, excludes wireless from the default SPM product, requires device-side confirmation, persists peer identifiers on both sides, bounds protocol frames, limits a session to one Mac, and provides deterministic transport shutdown. Merge commits and the upstream default-SPM CocoaAsyncSocket dependency were intentionally excluded. | Large or cross-repository features are ported on dedicated branches instead of being applied directly. Every imported patch must build against the fork's current default branch before it is merged. From e8643f7781fa935c4dedaae0ba98b26080b65c37 Mon Sep 17 00:00:00 2001 From: hongbo <1049145827@qq.com> Date: Sat, 18 Jul 2026 20:13:56 +0800 Subject: [PATCH 13/13] Harden wireless connection and trust persistence --- Package.swift | 18 ++++-- Src/Main/Shared/Channel/ECOSocketChannel.m | 73 ++++++++++++++++++++-- 2 files changed, 82 insertions(+), 9 deletions(-) diff --git a/Package.swift b/Package.swift index b6551502..18fba3e4 100644 --- a/Package.swift +++ b/Package.swift @@ -20,17 +20,21 @@ let package = Package( url: "https://github.com/swiftlang/swift-syntax.git", exact: "603.0.2" ), + .package( + url: "https://github.com/robbiehanson/CocoaAsyncSocket.git", + exact: "7.6.5" + ), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "LookinServer", - dependencies: [.target(name: "LookinServerSwift")], + dependencies: [ + .target(name: "LookinServerSwift"), + .product(name: "CocoaAsyncSocket", package: "CocoaAsyncSocket"), + ], path: "Src/Main", - // Wireless transport is intentionally CocoaPods-only for now. Keeping it - // out of the default product prevents local-network access in normal SPM builds. - exclude: ["Shared/Channel"], publicHeadersPath: "", cSettings: [ .headerSearchPath("**"), @@ -45,10 +49,14 @@ let package = Package( .headerSearchPath("Shared/Category"), .headerSearchPath("Shared/Message"), .headerSearchPath("Shared/Peertalk"), + .define("SHOULD_COMPILE_LOOKIN_SERVER", to: "1", .when(configuration: .debug)), + .define("SPM_LOOKIN_SERVER_ENABLED", to: "1", .when(configuration: .debug)), + .define("LOOKIN_SERVER_WIRELESS", to: "1", .when(configuration: .debug)), ], cxxSettings: [ .define("SHOULD_COMPILE_LOOKIN_SERVER", to: "1", .when(configuration: .debug)), - .define("SPM_LOOKIN_SERVER_ENABLED", to: "1", .when(configuration: .debug)) + .define("SPM_LOOKIN_SERVER_ENABLED", to: "1", .when(configuration: .debug)), + .define("LOOKIN_SERVER_WIRELESS", to: "1", .when(configuration: .debug)) ] ), .target( diff --git a/Src/Main/Shared/Channel/ECOSocketChannel.m b/Src/Main/Shared/Channel/ECOSocketChannel.m index 23a14d0d..09b6362f 100644 --- a/Src/Main/Shared/Channel/ECOSocketChannel.m +++ b/Src/Main/Shared/Channel/ECOSocketChannel.m @@ -41,6 +41,9 @@ @interface ECOSocketChannel() @property (nonatomic, strong) GCDAsyncSocket *cSocket; @property (nonatomic, strong) NSMutableArray *clientSockets; @property (nonatomic, assign) BOOL stopped; +#if TARGET_OS_IPHONE +@property (nonatomic, assign) BOOL browserReconnectScheduled; +#endif @end @@ -171,7 +174,15 @@ - (void)connectToAddresses:(NSArray *)addresses return; } GCDAsyncSocket *socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)]; - NSData *address = [addresses objectAtIndex:0]; + // Bonjour may return an unreachable IPv6 address before the LAN IPv4 address. + // Prefer IPv4 for the current local-network protocol and fall back to IPv6. + NSData *address = [addresses firstObject]; + for (NSData *candidate in addresses) { + if ([GCDAsyncSocket isIPv4Address:candidate]) { + address = candidate; + break; + } + } NSError *error = nil; BOOL connected = [socket connectToAddress:address error:&error]; if (connected) { @@ -415,6 +426,7 @@ - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)t tag == ECOSocketHeadTag_ClientListen) { NSUInteger delimiterLength = GCDAsyncSocket.CRLFData.length; if (data.length <= delimiterLength) { + NSLog(@">> [ECOSocketChannel] rejecting empty socket header"); [sock disconnect]; return; } @@ -427,6 +439,7 @@ - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)t NSDictionary *header = headerObject; NSInteger version = [header[@"version"] integerValue]; if (version != ECOSocketProtocolVersion) { + NSLog(@">> [ECOSocketChannel] rejecting protocol version: %@", header[@"version"]); [sock disconnect]; return; } @@ -458,6 +471,7 @@ - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)t if (tag == ECOSocketDataTag_Device) { ECOChannelDeviceInfo *deviceInfo = [[ECOChannelDeviceInfo alloc] initWithData:data]; if (![self hasValidIdentity:deviceInfo]) { + NSLog(@">> [ECOSocketChannel] rejecting peer without stable identity: %@", deviceInfo); [sock disconnect]; return; } @@ -493,7 +507,7 @@ - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)t //授权信息 ECOChannelDeviceInfo *deviceInfo = sock.userData; ECOChannelDeviceInfo *tempDevice = [[ECOChannelDeviceInfo alloc] initWithData:data]; - if (![self hasValidIdentity:deviceInfo] || ![self hasValidIdentity:tempDevice] || + if (![self hasValidIdentity:deviceInfo] || ![self authorizationDeviceMatchesLocalDevice:tempDevice] || tempDevice.authorizedType < ECOAuthorizeResponseType_Deny || tempDevice.authorizedType > ECOAuthorizeResponseType_AllowAlways) { [sock disconnect]; @@ -552,7 +566,9 @@ - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)t [self readHeaderFromSocket:sock tag:ECOSocketHeadTag_Data]; } - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err { -// NSLog(@"%s",__func__); + if (err) { + NSLog(@">> [ECOSocketChannel] socket disconnected: %@", err); + } [self p_lock]; [sock setDelegate:nil]; if ([self.sockets containsObject:sock]) { @@ -571,8 +587,32 @@ - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError * if (sock == self.mSocket) { [self restartListening]; } +#if TARGET_OS_IPHONE + [self scheduleBrowserReconnectIfNeeded]; +#endif } #pragma mark - helper +#if TARGET_OS_IPHONE +- (void)scheduleBrowserReconnectIfNeeded { + [self p_lock]; + BOOL shouldSchedule = !self.stopped && self.sockets.count == 0 && !self.browserReconnectScheduled; + if (shouldSchedule) { + self.browserReconnectScheduled = YES; + } + [self p_unlock]; + if (!shouldSchedule) { + return; + } + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(ECOSocketRetryListenDelay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + self.browserReconnectScheduled = NO; + if (!self.stopped && self.sockets.count == 0) { + [self.browser stopBrowsing]; + [self.browser startBrowsing]; + } + }); +} +#endif + - (void)readHeaderFromSocket:(GCDAsyncSocket *)socket tag:(long)tag { [socket readDataToData:GCDAsyncSocket.CRLFData withTimeout:-1 @@ -581,14 +621,39 @@ - (void)readHeaderFromSocket:(GCDAsyncSocket *)socket tag:(long)tag { } - (NSString *)trustedIdentifierForDevice:(ECOChannelDeviceInfo *)device { - if (!device.uuid.length || !device.appInfo.appId.length) { + if (!device.uuid.length) { + return nil; + } +#if TARGET_OS_OSX + if (!device.appInfo.appId.length) { return nil; } return [NSString stringWithFormat:@"%@_%@", device.uuid, device.appInfo.appId]; +#else + return device.uuid; +#endif } - (BOOL)hasValidIdentity:(ECOChannelDeviceInfo *)device { +#if TARGET_OS_OSX return device.uuid.length > 0 && device.appInfo.appId.length > 0; +#else + // The macOS client intentionally does not advertise an inspected-app identity. + return device.uuid.length > 0; +#endif +} + +- (BOOL)authorizationDeviceMatchesLocalDevice:(ECOChannelDeviceInfo *)device { + ECOChannelDeviceInfo *localDevice = [ECOChannelDeviceInfo new]; + if (!device.uuid.length || ![device.uuid isEqualToString:localDevice.uuid]) { + return NO; + } +#if TARGET_OS_IPHONE + return device.appInfo.appId.length > 0 && + [device.appInfo.appId isEqualToString:localDevice.appInfo.appId]; +#else + return YES; +#endif } - (BOOL)isPersistentlyTrustedDevice:(ECOChannelDeviceInfo *)device {