├── etc ├── peertalk-icon.pdf ├── peertalk-icon.png └── style.css ├── Peertalk iOS Example ├── Icon-72.png ├── Icon-72.psd ├── Icon-114@2x.png ├── Icon-114@2x.psd ├── Icon-144@2x.png ├── Icon-144@2x.psd ├── PTViewController.h ├── PTAppDelegate.h ├── PTAppDelegate.m ├── main.m ├── LaunchScreen.storyboard ├── Info.plist ├── Main.storyboard └── PTViewController.m ├── Peertalk macOS Example ├── Icon.icns ├── Icon-1024.psd ├── main.m ├── PTAppDelegate.h ├── Info.plist ├── PTExampleProtocol.h └── PTAppDelegate.m ├── peertalk ├── PTDefines.h ├── PTPrivate.h ├── Peertalk.h ├── Info.plist ├── PTUSBHub.h ├── PTProtocol.h ├── PTChannel.h ├── PTProtocol.m ├── PTChannel.m └── PTUSBHub.m ├── Peertalk Swift Example ├── Assets.xcassets │ ├── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── AppDelegate.swift ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── SceneDelegate.swift └── ViewController.swift ├── .gitignore ├── peertalk.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ └── rinov.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── Peertalk macOS Example.xcscheme │ ├── Peertalk iOS Swift Example.xcscheme │ └── Peertalk iOS.xcscheme ├── peertalk-tests ├── PTProtocolTests.h ├── Info.plist └── PTProtocolTests.m ├── LICENSE.txt ├── PeerTalk.podspec ├── README.md └── index.html /etc/peertalk-icon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/peertalk/HEAD/etc/peertalk-icon.pdf -------------------------------------------------------------------------------- /etc/peertalk-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/peertalk/HEAD/etc/peertalk-icon.png -------------------------------------------------------------------------------- /Peertalk iOS Example/Icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/peertalk/HEAD/Peertalk iOS Example/Icon-72.png -------------------------------------------------------------------------------- /Peertalk iOS Example/Icon-72.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/peertalk/HEAD/Peertalk iOS Example/Icon-72.psd -------------------------------------------------------------------------------- /Peertalk macOS Example/Icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/peertalk/HEAD/Peertalk macOS Example/Icon.icns -------------------------------------------------------------------------------- /Peertalk iOS Example/Icon-114@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/peertalk/HEAD/Peertalk iOS Example/Icon-114@2x.png -------------------------------------------------------------------------------- /Peertalk iOS Example/Icon-114@2x.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/peertalk/HEAD/Peertalk iOS Example/Icon-114@2x.psd -------------------------------------------------------------------------------- /Peertalk iOS Example/Icon-144@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/peertalk/HEAD/Peertalk iOS Example/Icon-144@2x.png -------------------------------------------------------------------------------- /Peertalk iOS Example/Icon-144@2x.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/peertalk/HEAD/Peertalk iOS Example/Icon-144@2x.psd -------------------------------------------------------------------------------- /Peertalk macOS Example/Icon-1024.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/peertalk/HEAD/Peertalk macOS Example/Icon-1024.psd -------------------------------------------------------------------------------- /peertalk/PTDefines.h: -------------------------------------------------------------------------------- 1 | #ifndef PT_FINAL 2 | #define PT_FINAL __attribute__((objc_subclassing_restricted)) 3 | #endif 4 | -------------------------------------------------------------------------------- /Peertalk Swift Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Peertalk iOS Example/PTViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "PTChannel.h" 3 | 4 | @interface PTViewController : UIViewController 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | *.sock* 4 | *.pyo 5 | *.pyc 6 | *.crash 7 | *.so 8 | *.o 9 | *.a 10 | * copy.* 11 | ._* 12 | *.patch 13 | *.diff 14 | 15 | xcuserdata 16 | -------------------------------------------------------------------------------- /Peertalk iOS Example/PTAppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface PTAppDelegate : UIResponder 4 | 5 | @property (strong, nonatomic) UIWindow *window; 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /Peertalk iOS Example/PTAppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "PTAppDelegate.h" 2 | 3 | @interface PTAppDelegate () 4 | @end 5 | 6 | @implementation PTAppDelegate 7 | 8 | @synthesize window = window_; 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /peertalk.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /peertalk.xcodeproj/project.xcworkspace/xcuserdata/rinov.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/peertalk/HEAD/peertalk.xcodeproj/project.xcworkspace/xcuserdata/rinov.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Peertalk Swift Example/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /peertalk/PTPrivate.h: -------------------------------------------------------------------------------- 1 | #ifndef PT_PRECISE_LIFETIME 2 | #define PT_PRECISE_LIFETIME __attribute__((objc_precise_lifetime)) 3 | #endif 4 | 5 | #ifndef PT_PRECISE_LIFETIME_UNUSED 6 | #define PT_PRECISE_LIFETIME_UNUSED __attribute__((objc_precise_lifetime, unused)) 7 | #endif 8 | -------------------------------------------------------------------------------- /peertalk.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Peertalk macOS Example/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // Peertalk Example 4 | // 5 | // Created by Rasmus Andersson on 3/30/12. 6 | // Copyright (c) 2012 __MyCompanyName__. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | return NSApplicationMain(argc, (const char **)argv); 14 | } 15 | -------------------------------------------------------------------------------- /peertalk-tests/PTProtocolTests.h: -------------------------------------------------------------------------------- 1 | #import 2 | #include 3 | #import "PTProtocol.h" 4 | #import "PTPrivate.h" 5 | 6 | @interface PTProtocolTests : XCTestCase { 7 | dispatch_fd_t socket_[2]; 8 | dispatch_queue_t queue_[2]; 9 | dispatch_io_t channel_[2]; 10 | PTProtocol *protocol_[2]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /peertalk/Peertalk.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for Peertalk. 4 | FOUNDATION_EXPORT double PeertalkVersionNumber; 5 | 6 | //! Project version string for Peertalk. 7 | FOUNDATION_EXPORT const unsigned char PeertalkVersionString[]; 8 | 9 | #import 10 | #import 11 | #import 12 | #import 13 | -------------------------------------------------------------------------------- /Peertalk iOS Example/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // Peertalk iOS Example 4 | // 5 | // Created by Rasmus Andersson on 3/30/12. 6 | // Copyright (c) 2012 __MyCompanyName__. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "PTAppDelegate.h" 12 | 13 | int main(int argc, char *argv[]) 14 | { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([PTAppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Peertalk macOS Example/PTAppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import 4 | 5 | static const NSTimeInterval PTAppReconnectDelay = 1.0; 6 | 7 | @interface PTAppDelegate : NSObject 8 | 9 | @property (assign) IBOutlet NSWindow *window; 10 | @property (assign) IBOutlet NSTextField *inputTextField; 11 | @property (assign) IBOutlet NSTextView *outputTextView; 12 | 13 | - (IBAction)sendMessage:(id)sender; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /peertalk-tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /peertalk/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Rasmus Andersson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /Peertalk macOS Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | Icon 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | ${MACOSX_DEPLOYMENT_TARGET} 27 | NSHumanReadableCopyright 28 | Copyright © 2012 __MyCompanyName__. All rights reserved. 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /PeerTalk.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'PeerTalk' 3 | spec.version = '0.1.0' 4 | spec.license = { :type => 'MIT', :file => 'LICENSE.txt' } 5 | spec.homepage = 'http://rsms.me/peertalk/' 6 | spec.authors = { 7 | 'Rasmus Andersson' => 'rasmus@notion.se', 8 | 'Jonathan Dann' => 'jonathan@jonathandann.com' 9 | } 10 | spec.summary = 'iOS and OS X Cocoa library for communicating over USB and TCP.' 11 | 12 | spec.source = { :git => "https://github.com/rsms/PeerTalk.git", :tag => '0.1.0' } 13 | spec.source_files = 'peertalk/*.{h,m}' 14 | spec.ios.deployment_target = '14.0' 15 | spec.osx.deployment_target = '11.0' 16 | 17 | spec.description = "PeerTalk is a iOS and OS X Cocoa library for communicating over USB and TCP.\n\n Highlights:\n\n * Provides you with USB device attach/detach events and attached device's info\n * Can connect to TCP services on supported attached devices (e.g. an iPhone), bridging the communication over USB transport\n * Offers a higher-level API (PTChannel and PTProtocol) for convenient implementations.\n* Tested and designed for libdispatch (aka Grand Central Dispatch).\n" 18 | 19 | spec.swift_version = ['5.0'] 20 | end 21 | -------------------------------------------------------------------------------- /Peertalk macOS Example/PTExampleProtocol.h: -------------------------------------------------------------------------------- 1 | #ifndef peertalk_PTExampleProtocol_h 2 | #define peertalk_PTExampleProtocol_h 3 | 4 | #import 5 | #include 6 | 7 | static const int PTExampleProtocolIPv4PortNumber = 2345; 8 | 9 | enum { 10 | PTExampleFrameTypeDeviceInfo = 100, 11 | PTExampleFrameTypeTextMessage = 101, 12 | PTExampleFrameTypePing = 102, 13 | PTExampleFrameTypePong = 103, 14 | }; 15 | 16 | typedef struct _PTExampleTextFrame { 17 | uint32_t length; 18 | uint8_t utf8text[0]; 19 | } PTExampleTextFrame; 20 | 21 | 22 | static dispatch_data_t PTExampleTextDispatchDataWithString(NSString *message) { 23 | // Use a custom struct 24 | const char *utf8text = [message cStringUsingEncoding:NSUTF8StringEncoding]; 25 | size_t length = strlen(utf8text); 26 | PTExampleTextFrame *textFrame = CFAllocatorAllocate(nil, sizeof(PTExampleTextFrame) + length, 0); 27 | memcpy(textFrame->utf8text, utf8text, length); // Copy bytes to utf8text array 28 | textFrame->length = htonl(length); // Convert integer to network byte order 29 | 30 | // Wrap the textFrame in a dispatch data object 31 | return dispatch_data_create((const void*)textFrame, sizeof(PTExampleTextFrame)+length, nil, ^{ 32 | CFAllocatorDeallocate(nil, textFrame); 33 | }); 34 | } 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /Peertalk Swift Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Peertalk Swift 4 | // 5 | // Created by Jonathan Dann on 04/12/2020. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Peertalk iOS Example/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Peertalk Swift Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Peertalk Swift Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Peertalk iOS Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | PeerTalk 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIcons 12 | 13 | CFBundlePrimaryIcon 14 | 15 | CFBundleIconFiles 16 | 17 | Icon-144@2x.png 18 | Icon-114@2x.png 19 | Icon-72.png 20 | 21 | UIPrerenderedIcon 22 | 23 | 24 | 25 | CFBundleIdentifier 26 | $(PRODUCT_BUNDLE_IDENTIFIER) 27 | CFBundleInfoDictionaryVersion 28 | 6.0 29 | CFBundleName 30 | ${PRODUCT_NAME} 31 | CFBundlePackageType 32 | APPL 33 | CFBundleShortVersionString 34 | 1.0 35 | CFBundleSignature 36 | ???? 37 | CFBundleVersion 38 | 1.0 39 | LSRequiresIPhoneOS 40 | 41 | UILaunchStoryboardName 42 | LaunchScreen 43 | UIMainStoryboardFile 44 | Main 45 | UIMainStoryboardFile~ipad 46 | MainStoryboard_iPad 47 | UIRequiredDeviceCapabilities 48 | 49 | armv7 50 | 51 | UIStatusBarStyle 52 | UIStatusBarStyleOpaqueBlack 53 | UISupportedInterfaceOrientations 54 | 55 | UIInterfaceOrientationPortrait 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | UISupportedInterfaceOrientations~ipad 60 | 61 | UIInterfaceOrientationPortrait 62 | UIInterfaceOrientationPortraitUpsideDown 63 | UIInterfaceOrientationLandscapeLeft 64 | UIInterfaceOrientationLandscapeRight 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /Peertalk Swift Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UIApplicationSupportsIndirectInputEvents 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /Peertalk Swift Example/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Peertalk Swift 4 | // 5 | // Created by Jonathan Dann on 04/12/2020. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /peertalk.xcodeproj/xcshareddata/xcschemes/Peertalk macOS Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /peertalk.xcodeproj/xcshareddata/xcschemes/Peertalk iOS Swift Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /peertalk/PTUSBHub.h: -------------------------------------------------------------------------------- 1 | #include 2 | #import 3 | 4 | // PTUSBDeviceDidAttachNotification 5 | // Posted when a device has been attached. Also posted for each device that is 6 | // already attached when the PTUSBHub starts listening. 7 | // 8 | // .userInfo = { 9 | // DeviceID = 3; 10 | // MessageType = Attached; 11 | // Properties = { 12 | // ConnectionSpeed = 480000000; 13 | // ConnectionType = USB; 14 | // DeviceID = 3; 15 | // LocationID = 1234567890; 16 | // ProductID = 1234; 17 | // SerialNumber = 0123456789abcdef0123456789abcdef01234567; 18 | // }; 19 | // } 20 | // 21 | FOUNDATION_EXPORT NSNotificationName const PTUSBDeviceDidAttachNotification NS_SWIFT_NAME(deviceDidAttach); 22 | 23 | // PTUSBDeviceDidDetachNotification 24 | // Posted when a device has been detached. 25 | // 26 | // .userInfo = { 27 | // DeviceID = 3; 28 | // MessageType = Detached; 29 | // } 30 | // 31 | FOUNDATION_EXPORT NSNotificationName const PTUSBDeviceDidDetachNotification NS_SWIFT_NAME(deviceDidDetach); 32 | 33 | typedef NSString * PTUSBHubNotificationKey NS_TYPED_ENUM; 34 | FOUNDATION_EXPORT PTUSBHubNotificationKey const PTUSBHubNotificationKeyDeviceID NS_SWIFT_NAME(deviceID); 35 | FOUNDATION_EXPORT PTUSBHubNotificationKey const PTUSBHubNotificationKeyMessageType NS_SWIFT_NAME(messageType); 36 | FOUNDATION_EXPORT PTUSBHubNotificationKey const PTUSBHubNotificationKeyProperties NS_SWIFT_NAME(properties); 37 | 38 | // NSError domain 39 | FOUNDATION_EXPORT NSString * const PTUSBHubErrorDomain; 40 | 41 | // Error codes returned with NSError.code for NSError domain PTUSBHubErrorDomain 42 | typedef enum { 43 | PTUSBHubErrorBadDevice = 2, 44 | PTUSBHubErrorConnectionRefused = 3, 45 | } PTUSBHubError; 46 | 47 | @interface PTUSBHub : NSObject 48 | 49 | // Shared, implicitly opened hub. 50 | + (instancetype)sharedHub; 51 | 52 | - (instancetype)init NS_UNAVAILABLE; 53 | 54 | // Connect to a TCP *port* on a device, while the actual transport is over USB. 55 | // Upon success, *error* is nil and *channel* is a duplex I/O channel. 56 | // You can retrieve the underlying file descriptor using 57 | // dispatch_io_get_descriptor(channel). The dispatch_io_t channel behaves just 58 | // like any stream type dispatch_io_t, making it possible to use the same logic 59 | // for both USB bridged connections and e.g. ethernet-based connections. 60 | // 61 | // *onStart* is called either when a connection failed, in which case the error 62 | // argument is non-nil, or when the connection was successfully established (the 63 | // error argument is nil). Must not be NULL. 64 | // 65 | // *onEnd* is called when a connection was open and just did close. If the error 66 | // argument is non-nil, the channel closed because of an error. Pass NULL for no 67 | // callback. 68 | // 69 | - (void)connectToDevice:(NSNumber*)deviceID 70 | port:(int)port 71 | onStart:(void(^)(NSError *error, dispatch_io_t channel))onStart 72 | onEnd:(void(^)(NSError *error))onEnd; 73 | 74 | // Start listening for devices. You only need to invoke this method on custom 75 | // instances to start receiving notifications. The shared instance returned from 76 | // +sharedHub is always in listening mode. 77 | // 78 | // *onStart* is called either when the system failed to start listening, in 79 | // which case the error argument is non-nil, or when the receiver is listening. 80 | // Pass NULL for no callback. 81 | // 82 | // *onEnd* is called when listening stopped. If the error argument is non-nil, 83 | // listening stopped because of an error. Pass NULL for no callback. 84 | // 85 | - (void)listenOnQueue:(dispatch_queue_t)queue 86 | onStart:(void(^)(NSError*))onStart 87 | onEnd:(void(^)(NSError*))onEnd; 88 | 89 | @end 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # peertalk 2 | 3 | PeerTalk is an iOS and Mac Cocoa library for communicating over USB. 4 | 5 | 6 | ┌──────────────────────────────┐ 7 | │ ┌──────────────────────────┐ │ 8 | │ │ │ │ 9 | ┌─────────┐ │ │ │ │ 10 | │┌───────┐│ │ │ Hello │ │ 11 | ││ ││ │ │ │ │ 12 | ││ Hello ││ │ │ │ │ 13 | ││ ││ │ │ │ │ 14 | │└───────┘│ │ └──────────────────────────┘ │ 15 | │ ◯ │ \ ─────────────────────────── \ 16 | └────╦────┘ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 17 | ║ ╔══════════■ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 18 | ╚═════════╝ \ ─────────────────────────── \ 19 | - meep - └─────────────────────────────┘ 20 | - beep - 21 | 22 | 23 | #### Highlights 24 | 25 | 1. Provides you with USB device attach/detach events and attached device's info 26 | 27 | 2. Can connect to TCP services on supported attached devices (e.g. an iPhone), 28 | bridging the communication over USB transport 29 | 30 | 3. Offers a higher-level API (PTChannel and PTProtocol) for convenient 31 | implementations. 32 | 33 | 4. Tested and designed for libdispatch (aka Grand Central Dispatch). 34 | 35 | Grab the goods from [https://github.com/rsms/peertalk](https://github.com/rsms/peertalk) 36 | 37 | 38 | ### Usage in Apple App Store 39 | 40 | PeerTalk has successfully been released on both the iOS and OS X app store. 41 | 42 | A great example is [Duet Display](http://www.duetdisplay.com/) which is a fantastic piece of software allowing you to use your iDevice as an extra display for your Mac using the Lightning or 30-pin cable. 43 | 44 | Facebook's [Origami](http://facebook.github.io/origami/) uses PeerTalk for it's Origami Live iOS app (in fact, this is where PeerTalk was first used, back in 2012) 45 | 46 | This *probably* means that you can use PeerTalk for apps aiming at the App Store. 47 | 48 | ## Getting started 49 | 50 | Suck down the code and open *peertalk.xcodeproj* in Xcode 4.3 or later on OS X 10.7 or later. 51 | 52 | 1. Select the "peertalk" target and hit Cmd+U (Product → Test) and verify that the unit tests passed. 53 | 54 | 2. Select the "Peertalk Example" target and hit Cmd+R (Product → Run). You should se a less than-pretty, standard window with some text saying it's ready. That's the OS X example app you're looking at. 55 | 56 | 3. In Xcode, select the "Peertalk iOS Example" target for the iPhone Simulator, and hit Cmd+R (Product → Run). There should be some action going on now. Try sending some messages between the OS X app and the app running in the iPhone simulator. 57 | 58 | 3. Connect your iOS device (iPhone, iPod or iPad) and kill the iPhone simulator and go back to Xcode. Select the "Peertalk iOS Example" target for your connected iOS device. Hit Cmd+R (Product → Run) to build and run the sample app on your device. 59 | 60 | It _should_ work. 61 | 62 | Demo video: [http://www.youtube.com/watch?v=kQPWy8N0mBg](http://www.youtube.com/watch?v=kQPWy8N0mBg) 63 | 64 | 65 | 66 | 67 | ### macOS sandboxed mode 68 | 69 | In https://github.com/rsms/peertalk/issues/36#issuecomment-596450033 @Lessica suggests the following entitlement snippet made macOS give access to peertalk: 70 | 71 | ```xml 72 | com.apple.security.temporary-exception.sbpl 73 | 74 | (allow network-outbound (literal "/private/var/run/usbmuxd")) 75 | 76 | ``` 77 | 78 | -------------------------------------------------------------------------------- /peertalk.xcodeproj/xcshareddata/xcschemes/Peertalk iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 63 | 69 | 70 | 71 | 72 | 78 | 79 | 85 | 86 | 87 | 88 | 90 | 91 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /etc/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | background: white; 3 | color: black; 4 | font: 16px/1.4 helvetica, arial, sans-serif; 5 | } 6 | body { 7 | display:block; 8 | margin: 0 auto; 9 | width: 800px; 10 | } 11 | 12 | h1,h2, h4,h5 { 13 | -webkit-font-smoothing: antialiased; 14 | text-rendering: optimizelegibility; 15 | } 16 | h1,h2,h3,h4,h5 { 17 | margin-top:1.618em; 18 | } 19 | 20 | h1 { color: #333; } 21 | h2 { color: #222; } 22 | h3 { color: #333; } 23 | h4 { color: #666; } 24 | 25 | a:link, a:active, a:visited { color: #0D2681; text-decoration:none; } 26 | a { text-decoration:none; } 27 | a:link, a:active { color:#316d96; } 28 | a:visited { color:#7e5494; } 29 | a:link:hover, a:active:hover, a:visited:hover { text-decoration:underline; } 30 | a:link:hover, a:active:hover { color:#006be4; } 31 | a:visited:hover { color:#af00cf; } 32 | a > img { margin-bottom:-1px; } 33 | a.img { border:none; background:transparent !important; } 34 | 35 | h1 a,h2 a,h3 a,h4 a,h5 a { 36 | color:inherit !important; 37 | } 38 | 39 | blockquote { 40 | border-left: 4px solid #E68E88; 41 | padding-left: 1em; 42 | margin-right: 1em; 43 | margin:0 1em 0 0; 44 | color: #664E4A; 45 | font-style: italic; 46 | } 47 | 48 | code, pre, tt, samp { font-family: 'Droid Sans Mono', 'Menlo', monospace; } 49 | code, pre { background-color: #f0f0f0; } 50 | code { color: inherit; } 51 | pre { 52 | color: #555; 53 | border-left: 2px solid #ddd; 54 | font-weight: lighter; 55 | padding: 1em; 56 | line-height:1.4; 57 | overflow-x: hidden; 58 | text-wrap: avoid; 59 | white-space: pre-wrap; 60 | } 61 | pre code { 62 | } 63 | 64 | :target { 65 | background-color:#FFFCB9; 66 | border-top-color:#F1E485; 67 | } 68 | 69 | span.str { color: #080; } 70 | span.kwd { color: #295AA4; } 71 | span.com { color: #999; } 72 | span.typ { color: #808; } 73 | span.lit { color: #066; } 74 | span.pun, span.opn, span.clo { color: #660; } 75 | span.pln { color: inherit; } 76 | span.tag { color: #008; } 77 | span.atn { color: #606; } 78 | span.atv { color: #080; } 79 | span.dec { color: #606; } 80 | /* Use higher contrast and font-styling for printable form. */ 81 | @media print { 82 | span.str { color: #060; } 83 | span.kwd { color: #006; font-weight: bold; } 84 | span.com { color: #999; font-style: italic; } 85 | span.typ { color: #404; font-weight: bold; } 86 | span.lit { color: #044; } 87 | span.pun, span.opn, span.clo { color: #440; } 88 | span.pln { color: #000; } 89 | span.tag { color: #006; font-weight: bold; } 90 | span.atn { color: #404; } 91 | span.atv { color: #060; } 92 | } 93 | 94 | #content h1 { 95 | text-align:center; 96 | padding-top:1em; 97 | margin-top:1em; 98 | } 99 | #content h1 a { 100 | display:block; 101 | } 102 | 103 | h2 { 104 | border-top: 2px solid #ddd; 105 | padding-top:0.5em; 106 | margin-top: 3em; 107 | } 108 | 109 | h3 { 110 | font-family: 'Droid Sans Mono', 'Menlo', monospace; 111 | font-weight: normal; 112 | color: black; 113 | line-height:1.5; 114 | padding:0; 115 | display:inline-block; 116 | border-radius: 4px; 117 | -webkit-border-radius: 4px; 118 | -moz-border-radius: 4px; 119 | margin-bottom:0; 120 | } 121 | h3 a { 122 | display: block; 123 | text-indent:-2em; 124 | padding:0.2em 0.4em; 125 | padding-left:2.4em; 126 | border-left: 0.2em solid #7B77E3; 127 | margin-left:-0.6em; 128 | } 129 | h3 .dimmed { 130 | color:#5260A1; 131 | } 132 | h3 a:hover { 133 | text-decoration: none !important; 134 | background: #F0F6FD; 135 | } 136 | 137 | .footnote { color: #525151; } 138 | .footnote:before { color: #525151; } 139 | .footnote:after { color: #525151; } 140 | div.footnotes { background: #F0F0F0; } 141 | 142 | footer { 143 | display:block; 144 | color: #999; 145 | padding:1em 0; 146 | margin-top:5em; 147 | border-top:1px solid #ddd; 148 | } 149 | 150 | #logo { 151 | cursor: default; 152 | } 153 | #logo div { 154 | font: bold 50px/70px helvetica, arial, sans-serif; 155 | text-align:center; 156 | /*-webkit-box-shadow: 0 2px 3px rgba(0,0,0,0.2);*/ 157 | } 158 | 159 | #github-banner img { 160 | position: absolute; top: 0; right: 0; border: 0; 161 | } 162 | 163 | /* iPhone */ 164 | @media only screen and (max-device-width: 480px) { 165 | html { font-size: 22px; } 166 | code { font-size:16px; font-weight:normal; color:#222; } 167 | #github-banner img { width:220px; height:220px; } 168 | #logo { zoom:1.5; } 169 | } -------------------------------------------------------------------------------- /Peertalk Swift Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Peertalk 3 | 4 | struct Example { 5 | struct Settings { 6 | static let port: in_port_t = 2345 7 | } 8 | enum Frame: UInt32 { 9 | case deviceInfo = 100 10 | case message = 101 11 | case ping = 102 12 | case pong = 103 13 | } 14 | } 15 | 16 | final class ViewController: UIViewController { 17 | @IBOutlet private var stackView: UIStackView! 18 | @IBOutlet private var textView: UITextView! 19 | @IBOutlet private var textField: UITextField! 20 | @IBOutlet private var bottomConstraint: NSLayoutConstraint! 21 | 22 | private var serverChannel: PTChannel? 23 | private var peerChannel: PTChannel? 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) 28 | textField.becomeFirstResponder() 29 | 30 | // Create a new channel that is listening on our IPv4 port 31 | let channel = PTChannel(protocol: nil, delegate: self) 32 | channel.listen(on: Example.Settings.port, IPv4Address: INADDR_LOOPBACK) { error in 33 | if let error = error { 34 | self.append(output: "Failed to listen on 127.0.0.1:\(Example.Settings.port) \(error)") 35 | } else { 36 | self.append(output: "Listening on 127.0.0.1:\(Example.Settings.port)") 37 | self.serverChannel = channel 38 | } 39 | } 40 | } 41 | 42 | @objc func keyboardWillShow(notification: Notification) { 43 | guard let keyboardEndFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { 44 | return 45 | } 46 | bottomConstraint.constant = -keyboardEndFrame.height 47 | } 48 | 49 | func send(message: String) { 50 | if let peerChannel = peerChannel { 51 | var m = message 52 | let payload = m.withUTF8 { buffer -> Data in 53 | var data = Data() 54 | data.append(CFSwapInt32HostToBig(UInt32(buffer.count)).data) 55 | data.append(buffer) 56 | return data 57 | } 58 | peerChannel.sendFrame(type: Example.Frame.message.rawValue, tag: 0, payload: payload, callback: nil) 59 | } else { 60 | append(output: "Cannot send message - not connected") 61 | } 62 | } 63 | 64 | func append(output message: String) { 65 | var text = textView.text ?? "" 66 | if text.count == 0 { 67 | text.append(message) 68 | } else { 69 | text.append("\n\(message)") 70 | } 71 | textView.text = text 72 | textView.scrollRangeToVisible(NSRange(location: text.count, length: 0)) 73 | } 74 | } 75 | 76 | extension ViewController: UITextFieldDelegate { 77 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 78 | guard peerChannel != nil, 79 | let message = textField.text else { 80 | return false 81 | } 82 | send(message: message) 83 | textField.text = nil 84 | return true 85 | } 86 | } 87 | 88 | extension ViewController: PTChannelDelegate { 89 | func channel(_ channel: PTChannel, didRecieveFrame type: UInt32, tag: UInt32, payload: Data?) { 90 | if let type = Example.Frame(rawValue: type) { 91 | switch type { 92 | case .message: 93 | guard let payload = payload else { 94 | return 95 | } 96 | payload.withUnsafeBytes { buffer in 97 | let textBytes = buffer[(buffer.startIndex + MemoryLayout.size)...] 98 | if let message = String(bytes: textBytes, encoding: .utf8) { 99 | append(output: "[\(channel.userInfo)] \(message)") 100 | } 101 | } 102 | case .ping: 103 | peerChannel?.sendFrame(type: Example.Frame.pong.rawValue, tag: 0, payload: nil, callback: nil) 104 | default: 105 | break 106 | } 107 | } 108 | } 109 | 110 | func channel(_ channel: PTChannel, shouldAcceptFrame type: UInt32, tag: UInt32, payloadSize: UInt32) -> Bool { 111 | guard channel == peerChannel else { 112 | return false 113 | } 114 | guard let frame = Example.Frame(rawValue: type), 115 | frame == .ping || frame == .message else { 116 | print("Unexpected frame of type: \(type)") 117 | return false 118 | } 119 | return true 120 | } 121 | 122 | func channel(_ channel: PTChannel, didAcceptConnection otherChannel: PTChannel, from address: PTAddress) { 123 | peerChannel?.cancel() 124 | peerChannel = otherChannel 125 | peerChannel?.userInfo = address 126 | self.append(output: "Connected to \(address)") 127 | } 128 | 129 | func channelDidEnd(_ channel: PTChannel, error: Error?) { 130 | if let error = error { 131 | append(output: "\(channel) ended with \(error)") 132 | } else { 133 | append(output: "Disconnected from \(channel.userInfo)") 134 | } 135 | } 136 | } 137 | 138 | extension FixedWidthInteger { 139 | var data: Data { 140 | var bytes = self 141 | return Data(bytes: &bytes, count: MemoryLayout.size(ofValue: self)) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /peertalk/PTProtocol.h: -------------------------------------------------------------------------------- 1 | // 2 | // A universal frame-based communication protocol which can be used to exchange 3 | // arbitrary structured data. 4 | // 5 | // In short: 6 | // 7 | // - Each transmission is comprised by one fixed-size frame. 8 | // - Each frame contains a protocol version number. 9 | // - Each frame contains an application frame type. 10 | // - Each frame can contain an identifying tag. 11 | // - Each frame can have application-specific data of up to UINT32_MAX size. 12 | // - Transactions style messaging can be modeled on top using frame tags. 13 | // - Lightweight API on top of libdispatch (aka GCD) -- close to the metal. 14 | // 15 | #include 16 | #import 17 | 18 | // Special frame tag that signifies "no tag". Your implementation should never 19 | // create a reply for a frame with this tag. 20 | static const uint32_t PTFrameNoTag = 0; 21 | 22 | // Special frame type that signifies that the stream has ended. 23 | static const uint32_t PTFrameTypeEndOfStream = 0; 24 | 25 | // NSError domain 26 | FOUNDATION_EXPORT NSString * const PTProtocolErrorDomain; 27 | 28 | 29 | @interface PTProtocol : NSObject 30 | 31 | // Queue on which to run data processing blocks. 32 | @property dispatch_queue_t queue; 33 | 34 | // Get the shared protocol object for *queue* 35 | + (PTProtocol*)sharedProtocolForQueue:(dispatch_queue_t)queue; 36 | 37 | // Initialize a new protocol object to use a specific queue. 38 | - (id)initWithDispatchQueue:(dispatch_queue_t)queue; 39 | 40 | // Initialize a new protocol object to use the current calling queue. 41 | - (id)init; 42 | 43 | #pragma mark Sending frames 44 | 45 | // Generate a new tag that is unique within this protocol object. 46 | - (uint32_t)newTag; 47 | 48 | // Send a frame over *channel* with an optional payload and optional callback. 49 | // If *callback* is not NULL, the block is invoked when either an error occured 50 | // or when the frame (and payload, if any) has been completely sent. 51 | - (void)sendFrameOfType:(uint32_t)frameType 52 | tag:(uint32_t)tag 53 | withPayload:(dispatch_data_t)payload 54 | overChannel:(dispatch_io_t)channel 55 | callback:(void(^)(NSError *error))callback; 56 | 57 | #pragma mark Receiving frames 58 | 59 | // Read frames over *channel* as they arrive. 60 | // The onFrame handler is responsible for reading (or discarding) any payload 61 | // and call *resumeReadingFrames* afterwards to resume reading frames. 62 | // To stop reading frames, simply do not invoke *resumeReadingFrames*. 63 | // When the stream ends, a frame of type PTFrameTypeEndOfStream is received. 64 | - (void)readFramesOverChannel:(dispatch_io_t)channel 65 | onFrame:(void(^)(NSError *error, 66 | uint32_t type, 67 | uint32_t tag, 68 | uint32_t payloadSize, 69 | dispatch_block_t resumeReadingFrames))onFrame; 70 | 71 | // Read a single frame over *channel*. A frame of type PTFrameTypeEndOfStream 72 | // denotes the stream has ended. 73 | - (void)readFrameOverChannel:(dispatch_io_t)channel 74 | callback:(void(^)(NSError *error, 75 | uint32_t frameType, 76 | uint32_t frameTag, 77 | uint32_t payloadSize))callback; 78 | 79 | #pragma mark Receiving frame payloads 80 | 81 | // Read a complete payload. It's the callers responsibility to make sure 82 | // payloadSize is not too large since memory will be automatically allocated 83 | // where only payloadSize is the limit. 84 | // The returned dispatch_data_t object owns *buffer* and thus you need to call 85 | // dispatch_retain on *contiguousData* if you plan to keep *buffer* around after 86 | // returning from the callback. 87 | - (void)readPayloadOfSize:(size_t)payloadSize 88 | overChannel:(dispatch_io_t)channel 89 | callback:(void(^)(NSError *error, 90 | dispatch_data_t contiguousData, 91 | const uint8_t *buffer, 92 | size_t bufferSize))callback; 93 | 94 | // Discard data of *size* waiting on *channel*. *callback* can be NULL. 95 | - (void)readAndDiscardDataOfSize:(size_t)size 96 | overChannel:(dispatch_io_t)channel 97 | callback:(void(^)(NSError *error, BOOL endOfStream))callback; 98 | 99 | @end 100 | 101 | @interface NSData (PTProtocol) 102 | // Creates a new dispatch_data_t object which references the receiver and uses 103 | // the receivers bytes as its backing data. The returned dispatch_data_t object 104 | // holds a reference to the recevier. It's the callers responsibility to call 105 | // dispatch_release on the returned object when done. 106 | - (dispatch_data_t)createReferencingDispatchData; 107 | + (NSData *)dataWithContentsOfDispatchData:(dispatch_data_t)data; 108 | + (NSDictionary *)dictionaryWithContentsOfData:(NSData *)data; 109 | @end 110 | 111 | @interface NSDictionary (PTProtocol) 112 | // See description of -[NSData(PTProtocol) createReferencingDispatchData] 113 | - (dispatch_data_t)createReferencingDispatchData; 114 | @end 115 | -------------------------------------------------------------------------------- /Peertalk Swift Example/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /Peertalk iOS Example/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /peertalk/PTChannel.h: -------------------------------------------------------------------------------- 1 | // 2 | // Represents a communication channel between two endpoints talking the same 3 | // PTProtocol. 4 | // 5 | #import 6 | #import 7 | #import 8 | #import 9 | 10 | #import 11 | #import 12 | #import 13 | 14 | @class PTAddress; 15 | @protocol PTChannelDelegate; 16 | 17 | NS_ASSUME_NONNULL_BEGIN 18 | 19 | PT_FINAL @interface PTChannel : NSObject 20 | 21 | // Delegate 22 | @property (strong, nullable) id delegate; 23 | 24 | // Communication protocol. 25 | @property PTProtocol *protocol; 26 | 27 | // YES if this channel is a listening server 28 | @property (readonly) BOOL isListening; 29 | 30 | // YES if this channel is a connected peer 31 | @property (readonly) BOOL isConnected; 32 | 33 | // Arbitrary attachment. Note that if you set this, the object will grow by 34 | // 8 bytes (64 bits). 35 | @property (strong) id userInfo; 36 | 37 | // Create a new channel using the shared PTProtocol for the current dispatch 38 | // queue, with *delegate*. 39 | + (PTChannel *)channelWithDelegate:(nullable id)delegate NS_SWIFT_UNAVAILABLE(""); 40 | 41 | // Initialize a new frame channel, configuring it to use the calling queue's 42 | // protocol instance (as returned by [PTProtocol sharedProtocolForQueue: 43 | // dispatch_get_current_queue()]) 44 | - (id)init NS_SWIFT_UNAVAILABLE(""); 45 | 46 | //// Initialize a new frame channel with a specific protocol. 47 | - (id)initWithProtocol:(PTProtocol *)protocol NS_SWIFT_UNAVAILABLE(""); 48 | 49 | // Initialize a new frame channel with a specific protocol and delegate. 50 | - (id)initWithProtocol:(nullable PTProtocol *)protocol delegate:(nullable id)delegate NS_SWIFT_NAME(init(protocol:delegate:)); 51 | 52 | // Connect to a TCP port on a device connected over USB 53 | - (void)connectToPort:(int)port overUSBHub:(PTUSBHub *)usbHub deviceID:(NSNumber *)deviceID callback:(void(^)(NSError * _Nullable error))callback NS_SWIFT_NAME(connect(to:over:deviceID:callback:)); 54 | 55 | // Connect to a TCP port at IPv4 address. Provided port must NOT be in network 56 | // byte order. Provided in_addr_t must NOT be in network byte order. A value returned 57 | // from inet_aton() will be in network byte order. You can use a value of inet_aton() 58 | // as the address parameter here, but you must flip the byte order before passing the 59 | // in_addr_t to this function. 60 | - (void)connectToPort:(in_port_t)port IPv4Address:(in_addr_t)address callback:(void(^)(NSError * _Nullable error, PTAddress *_Nullable address))callback NS_SWIFT_NAME(connect(to:IPv4Address:callback:)); 61 | 62 | // Listen for connections on port and address, effectively starting a socket 63 | // server. Provided port must NOT be in network byte order. Provided in_addr_t 64 | // must NOT be in network byte order. 65 | // For this to make sense, you should provide a onAccept block handler 66 | // or a delegate implementing ioFrameChannel:didAcceptConnection:. 67 | - (void)listenOnPort:(in_port_t)port IPv4Address:(in_addr_t)address callback:(void(^)(NSError * _Nullable error))callback NS_SWIFT_NAME(listen(on:IPv4Address:callback:)); 68 | 69 | // Send a frame with an optional payload and optional callback. 70 | // If *callback* is not NULL, the block is invoked when either an error occured 71 | // or when the frame (and payload, if any) has been completely sent. 72 | - (void)sendFrameOfType:(uint32_t)frameType tag:(uint32_t)tag withPayload:(nullable NSData *)payload callback:(nullable void(^)(NSError * _Nullable error))callback NS_SWIFT_NAME(sendFrame(type:tag:payload:callback:)); 73 | 74 | // Lower-level method to assign a connected dispatch IO channel to this channel 75 | - (BOOL)startReadingFromConnectedChannel:(dispatch_io_t)channel error:(__autoreleasing NSError **)error NS_SWIFT_NAME(startReading(from:)); 76 | 77 | // Close the channel, preventing further reading and writing. Any ongoing and 78 | // queued reads and writes will be aborted. 79 | - (void)close; 80 | 81 | // "graceful" close -- any ongoing and queued reads and writes will complete 82 | // before the channel ends. 83 | - (void)cancel; 84 | 85 | @end 86 | 87 | // Represents a peer's address 88 | PT_FINAL @interface PTAddress : NSObject 89 | // For network addresses, this is the IP address in textual format 90 | @property (readonly) NSString *name; 91 | // For network addresses, this is the port number. Otherwise 0 (zero). 92 | @property (readonly) NSInteger port; 93 | @end 94 | 95 | 96 | // Protocol for PTChannel delegates 97 | @protocol PTChannelDelegate 98 | 99 | @required 100 | // Invoked when a new frame has arrived on a channel. 101 | - (void)ioFrameChannel:(PTChannel *)channel didReceiveFrameOfType:(uint32_t)type tag:(uint32_t)tag payload:(nullable NSData *)payload NS_SWIFT_NAME(channel(_:didRecieveFrame:tag:payload:)); 102 | 103 | @optional 104 | // Invoked to accept an incoming frame on a channel. Reply NO ignore the 105 | // incoming frame. If not implemented by the delegate, all frames are accepted. 106 | - (BOOL)ioFrameChannel:(PTChannel *)channel shouldAcceptFrameOfType:(uint32_t)type tag:(uint32_t)tag payloadSize:(uint32_t)payloadSize NS_SWIFT_NAME(channel(_:shouldAcceptFrame:tag:payloadSize:));; 107 | 108 | // Invoked when the channel closed. If it closed because of an error, *error* is 109 | // a non-nil NSError object. 110 | - (void)ioFrameChannel:(PTChannel *)channel didEndWithError:(nullable NSError *)error NS_SWIFT_NAME(channelDidEnd(_:error:)); 111 | 112 | // For listening channels, this method is invoked when a new connection has been 113 | // accepted. 114 | - (void)ioFrameChannel:(PTChannel *)channel didAcceptConnection:(PTChannel *)otherChannel fromAddress:(PTAddress *)address NS_SWIFT_NAME(channel(_:didAcceptConnection:from:)); 115 | 116 | @end 117 | 118 | NS_ASSUME_NONNULL_END 119 | -------------------------------------------------------------------------------- /Peertalk iOS Example/PTViewController.m: -------------------------------------------------------------------------------- 1 | #import "PTExampleProtocol.h" 2 | #import "PTViewController.h" 3 | 4 | @interface PTViewController () < 5 | PTChannelDelegate, 6 | UITextFieldDelegate 7 | > { 8 | __weak PTChannel *serverChannel_; 9 | __weak PTChannel *peerChannel_; 10 | } 11 | @property (nonatomic) IBOutlet UIStackView *stackView; 12 | @property (nonatomic) IBOutlet UITextView *outputTextView; 13 | @property (nonatomic) IBOutlet UITextField *inputTextField; 14 | @property (nonatomic) IBOutlet NSLayoutConstraint *bottomConstraint; 15 | @end 16 | 17 | @implementation PTViewController 18 | 19 | @synthesize stackView = stackView_; 20 | @synthesize bottomConstraint = bottomConstraint_; 21 | @synthesize outputTextView = outputTextView_; 22 | @synthesize inputTextField = inputTextField_; 23 | 24 | - (void)viewDidLoad { 25 | [super viewDidLoad]; 26 | 27 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; 28 | 29 | // Setup UI 30 | [inputTextField_ becomeFirstResponder]; 31 | 32 | // Create a new channel that is listening on our IPv4 port 33 | PTChannel *channel = [PTChannel channelWithDelegate:self]; 34 | [channel listenOnPort:PTExampleProtocolIPv4PortNumber IPv4Address:INADDR_LOOPBACK callback:^(NSError *error) { 35 | if (error) { 36 | [self appendOutputMessage:[NSString stringWithFormat:@"Failed to listen on 127.0.0.1:%d: %@", PTExampleProtocolIPv4PortNumber, error]]; 37 | } else { 38 | [self appendOutputMessage:[NSString stringWithFormat:@"Listening on 127.0.0.1:%d", PTExampleProtocolIPv4PortNumber]]; 39 | self->serverChannel_ = channel; 40 | } 41 | }]; 42 | } 43 | 44 | - (void)keyboardWillShow:(NSNotification *)notification { 45 | CGRect keybordEndFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; 46 | bottomConstraint_.constant = -keybordEndFrame.size.height; 47 | } 48 | 49 | - (UIInterfaceOrientationMask)supportedInterfaceOrientations { 50 | return UIInterfaceOrientationMaskPortrait; 51 | } 52 | 53 | - (BOOL)textFieldShouldReturn:(UITextField *)textField { 54 | if (peerChannel_) { 55 | [self sendMessage:inputTextField_.text]; 56 | inputTextField_.text = @""; 57 | return NO; 58 | } else { 59 | return YES; 60 | } 61 | } 62 | 63 | - (void)sendMessage:(NSString*)message { 64 | if (peerChannel_) { 65 | dispatch_data_t payload = PTExampleTextDispatchDataWithString(message); 66 | [peerChannel_ sendFrameOfType:PTExampleFrameTypeTextMessage tag:PTFrameNoTag withPayload:(NSData *)payload callback:^(NSError *error) { 67 | if (error) { 68 | NSLog(@"Failed to send message: %@", error); 69 | } 70 | }]; 71 | [self appendOutputMessage:[NSString stringWithFormat:@"[you]: %@", message]]; 72 | } else { 73 | [self appendOutputMessage:@"Can not send message — not connected"]; 74 | } 75 | } 76 | 77 | - (void)appendOutputMessage:(NSString*)message { 78 | NSLog(@">> %@", message); 79 | NSString *text = outputTextView_.text; 80 | if (text.length == 0) { 81 | outputTextView_.text = [text stringByAppendingString:message]; 82 | } else { 83 | outputTextView_.text = [text stringByAppendingFormat:@"\n%@", message]; 84 | [outputTextView_ scrollRangeToVisible:NSMakeRange(outputTextView_.text.length, 0)]; 85 | } 86 | } 87 | 88 | 89 | #pragma mark - Communicating 90 | 91 | - (void)sendDeviceInfo { 92 | if (!peerChannel_) { 93 | return; 94 | } 95 | 96 | NSLog(@"Sending device info over %@", peerChannel_); 97 | 98 | UIScreen *screen = [UIScreen mainScreen]; 99 | CGSize screenSize = screen.bounds.size; 100 | NSDictionary *screenSizeDict = (__bridge_transfer NSDictionary*)CGSizeCreateDictionaryRepresentation(screenSize); 101 | UIDevice *device = [UIDevice currentDevice]; 102 | NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys: 103 | device.localizedModel, @"localizedModel", 104 | [NSNumber numberWithBool:device.multitaskingSupported], @"multitaskingSupported", 105 | device.name, @"name", 106 | (UIDeviceOrientationIsLandscape(device.orientation) ? @"landscape" : @"portrait"), @"orientation", 107 | device.systemName, @"systemName", 108 | device.systemVersion, @"systemVersion", 109 | screenSizeDict, @"screenSize", 110 | [NSNumber numberWithDouble:screen.scale], @"screenScale", 111 | nil]; 112 | dispatch_data_t payload = [info createReferencingDispatchData]; 113 | [peerChannel_ sendFrameOfType:PTExampleFrameTypeDeviceInfo tag:PTFrameNoTag withPayload:(NSData *)payload callback:^(NSError *error) { 114 | if (error) { 115 | NSLog(@"Failed to send PTExampleFrameTypeDeviceInfo: %@", error); 116 | } 117 | }]; 118 | } 119 | 120 | 121 | #pragma mark - PTChannelDelegate 122 | 123 | // Invoked to accept an incoming frame on a channel. Reply NO ignore the 124 | // incoming frame. If not implemented by the delegate, all frames are accepted. 125 | - (BOOL)ioFrameChannel:(PTChannel*)channel shouldAcceptFrameOfType:(uint32_t)type tag:(uint32_t)tag payloadSize:(uint32_t)payloadSize { 126 | if (channel != peerChannel_) { 127 | // A previous channel that has been canceled but not yet ended. Ignore. 128 | return NO; 129 | } else if (type != PTExampleFrameTypeTextMessage && type != PTExampleFrameTypePing) { 130 | NSLog(@"Unexpected frame of type %u", type); 131 | [channel close]; 132 | return NO; 133 | } else { 134 | return YES; 135 | } 136 | } 137 | 138 | // Invoked when a new frame has arrived on a channel. 139 | - (void)ioFrameChannel:(PTChannel*)channel didReceiveFrameOfType:(uint32_t)type tag:(uint32_t)tag payload:(NSData *)payload { 140 | if (type == PTExampleFrameTypeTextMessage) { 141 | PTExampleTextFrame *textFrame = (PTExampleTextFrame*)payload.bytes; 142 | textFrame->length = ntohl(textFrame->length); 143 | NSString *message = [[NSString alloc] initWithBytes:textFrame->utf8text length:textFrame->length encoding:NSUTF8StringEncoding]; 144 | [self appendOutputMessage:[NSString stringWithFormat:@"[%@]: %@", channel.userInfo, message]]; 145 | } else if (type == PTExampleFrameTypePing && peerChannel_) { 146 | [peerChannel_ sendFrameOfType:PTExampleFrameTypePong tag:tag withPayload:nil callback:nil]; 147 | } 148 | } 149 | 150 | // Invoked when the channel closed. If it closed because of an error, *error* is 151 | // a non-nil NSError object. 152 | - (void)ioFrameChannel:(PTChannel*)channel didEndWithError:(NSError*)error { 153 | if (error) { 154 | [self appendOutputMessage:[NSString stringWithFormat:@"%@ ended with error: %@", channel, error]]; 155 | } else { 156 | [self appendOutputMessage:[NSString stringWithFormat:@"Disconnected from %@", channel.userInfo]]; 157 | } 158 | } 159 | 160 | // For listening channels, this method is invoked when a new connection has been 161 | // accepted. 162 | - (void)ioFrameChannel:(PTChannel*)channel didAcceptConnection:(PTChannel*)otherChannel fromAddress:(PTAddress*)address { 163 | // Cancel any other connection. We are FIFO, so the last connection 164 | // established will cancel any previous connection and "take its place". 165 | if (peerChannel_) { 166 | [peerChannel_ cancel]; 167 | } 168 | 169 | // Weak pointer to current connection. Connection objects live by themselves 170 | // (owned by its parent dispatch queue) until they are closed. 171 | peerChannel_ = otherChannel; 172 | peerChannel_.userInfo = address; 173 | [self appendOutputMessage:[NSString stringWithFormat:@"Connected to %@", address]]; 174 | 175 | // Send some information about ourselves to the other end 176 | [self sendDeviceInfo]; 177 | } 178 | 179 | 180 | @end 181 | -------------------------------------------------------------------------------- /peertalk/PTProtocol.m: -------------------------------------------------------------------------------- 1 | #import "PTProtocol.h" 2 | #import "PTPrivate.h" 3 | #import 4 | 5 | static const uint32_t PTProtocolVersion1 = 1; 6 | 7 | NSString * const PTProtocolErrorDomain = @"PTProtocolError"; 8 | 9 | // This is what we send as the header for each frame. 10 | typedef struct _PTFrame { 11 | // The version of the frame and protocol. 12 | uint32_t version; 13 | 14 | // Type of frame 15 | uint32_t type; 16 | 17 | // Unless zero, a tag is retained in frames that are responses to previous 18 | // frames. Applications can use this to build transactions or request-response 19 | // logic. 20 | uint32_t tag; 21 | 22 | // If payloadSize is larger than zero, *payloadSize* number of bytes are 23 | // following, constituting application-specific data. 24 | uint32_t payloadSize; 25 | 26 | } PTFrame; 27 | 28 | 29 | @interface PTProtocol () { 30 | uint32_t nextFrameTag_; 31 | @public 32 | dispatch_queue_t queue_; 33 | } 34 | - (dispatch_data_t)createDispatchDataWithFrameOfType:(uint32_t)type frameTag:(uint32_t)frameTag payload:(dispatch_data_t)payload; 35 | @end 36 | 37 | 38 | static void _release_queue_local_protocol(void *objcobj) { 39 | if (objcobj) { 40 | PTProtocol *protocol = (__bridge_transfer id)objcobj; 41 | protocol->queue_ = NULL; 42 | } 43 | } 44 | 45 | 46 | @interface RQueueLocalIOFrameProtocol : PTProtocol 47 | @end 48 | @implementation RQueueLocalIOFrameProtocol 49 | - (void)setQueue:(dispatch_queue_t)queue { 50 | } 51 | @end 52 | 53 | 54 | @implementation PTProtocol 55 | 56 | 57 | + (PTProtocol*)sharedProtocolForQueue:(dispatch_queue_t)queue { 58 | static const char currentQueueFrameProtocolKey; 59 | PTProtocol *currentQueueFrameProtocol = (__bridge PTProtocol*)dispatch_queue_get_specific(queue, ¤tQueueFrameProtocolKey); 60 | if (!currentQueueFrameProtocol) { 61 | currentQueueFrameProtocol = [[RQueueLocalIOFrameProtocol alloc] initWithDispatchQueue:NULL]; 62 | currentQueueFrameProtocol->queue_ = queue; // reference, no retain, since we would create cyclic references 63 | dispatch_queue_set_specific(queue, ¤tQueueFrameProtocolKey, (__bridge_retained void*)currentQueueFrameProtocol, &_release_queue_local_protocol); 64 | return (__bridge PTProtocol*)dispatch_queue_get_specific(queue, ¤tQueueFrameProtocolKey); // to avoid race conds 65 | } else { 66 | return currentQueueFrameProtocol; 67 | } 68 | } 69 | 70 | 71 | - (id)initWithDispatchQueue:(dispatch_queue_t)queue { 72 | if (!(self = [super init])) return nil; 73 | queue_ = queue; 74 | return self; 75 | } 76 | 77 | - (id)init { 78 | return [self initWithDispatchQueue:dispatch_get_main_queue()]; 79 | } 80 | 81 | - (dispatch_queue_t)queue { 82 | return queue_; 83 | } 84 | 85 | - (void)setQueue:(dispatch_queue_t)queue { 86 | queue_ = queue; 87 | } 88 | 89 | 90 | - (uint32_t)newTag { 91 | return ++nextFrameTag_; 92 | } 93 | 94 | 95 | #pragma mark - 96 | #pragma mark Creating frames 97 | 98 | 99 | - (dispatch_data_t)createDispatchDataWithFrameOfType:(uint32_t)type frameTag:(uint32_t)frameTag payload:(dispatch_data_t)payload { 100 | PTFrame *frame = CFAllocatorAllocate(kCFAllocatorDefault, sizeof(PTFrame), 0); 101 | frame->version = htonl(PTProtocolVersion1); 102 | frame->type = htonl(type); 103 | frame->tag = htonl(frameTag); 104 | 105 | if (payload) { 106 | size_t payloadSize = dispatch_data_get_size(payload); 107 | assert(payloadSize <= UINT32_MAX); 108 | frame->payloadSize = htonl((uint32_t)payloadSize); 109 | } else { 110 | frame->payloadSize = 0; 111 | } 112 | 113 | dispatch_data_t frameData = dispatch_data_create((const void*)frame, sizeof(PTFrame), queue_, ^{ 114 | CFAllocatorDeallocate(kCFAllocatorDefault, (void*)frame); 115 | }); 116 | 117 | if (payload && frame->payloadSize != 0) { 118 | // chain frame + payload 119 | dispatch_data_t data = dispatch_data_create_concat(frameData, payload); 120 | frameData = data; 121 | } 122 | 123 | return frameData; 124 | } 125 | 126 | 127 | #pragma mark - 128 | #pragma mark Sending frames 129 | 130 | 131 | - (void)sendFrameOfType:(uint32_t)frameType tag:(uint32_t)tag withPayload:(dispatch_data_t)payload overChannel:(dispatch_io_t)channel callback:(void(^)(NSError*))callback { 132 | dispatch_data_t frame = [self createDispatchDataWithFrameOfType:frameType frameTag:tag payload:payload]; 133 | dispatch_io_write(channel, 0, frame, queue_, ^(bool done, dispatch_data_t data, int _errno) { 134 | if (done && callback) { 135 | callback(_errno == 0 ? nil : [[NSError alloc] initWithDomain:NSPOSIXErrorDomain code:_errno userInfo:nil]); 136 | } 137 | }); 138 | } 139 | 140 | 141 | #pragma mark - 142 | #pragma mark Receiving frames 143 | 144 | 145 | - (void)readFrameOverChannel:(dispatch_io_t)channel callback:(void(^)(NSError *error, uint32_t frameType, uint32_t frameTag, uint32_t payloadSize))callback { 146 | __block dispatch_data_t allData = NULL; 147 | 148 | dispatch_io_read(channel, 0, sizeof(PTFrame), queue_, ^(bool done, dispatch_data_t data, int error) { 149 | size_t dataSize = data ? dispatch_data_get_size(data) : 0; 150 | 151 | if (dataSize) { 152 | if (!allData) { 153 | allData = data; 154 | } else { 155 | allData = dispatch_data_create_concat(allData, data); 156 | } 157 | } 158 | 159 | if (done) { 160 | if (error != 0) { 161 | callback([[NSError alloc] initWithDomain:NSPOSIXErrorDomain code:error userInfo:nil], 0, 0, 0); 162 | return; 163 | } 164 | 165 | if (dataSize == 0) { 166 | callback(nil, PTFrameTypeEndOfStream, 0, 0); 167 | return; 168 | } 169 | 170 | if (!allData || dispatch_data_get_size(allData) < sizeof(PTFrame)) { 171 | callback([[NSError alloc] initWithDomain:PTProtocolErrorDomain code:0 userInfo:nil], 0, 0, 0); 172 | return; 173 | } 174 | 175 | PTFrame *frame = NULL; 176 | size_t size = 0; 177 | 178 | PT_PRECISE_LIFETIME dispatch_data_t contiguousData = dispatch_data_create_map(allData, (const void **)&frame, &size); // precise lifetime guarantees bytes in frame will stay valid till the end of scope 179 | if (!contiguousData) { 180 | callback([[NSError alloc] initWithDomain:NSPOSIXErrorDomain code:ENOMEM userInfo:nil], 0, 0, 0); 181 | return; 182 | } 183 | 184 | frame->version = ntohl(frame->version); 185 | if (frame->version != PTProtocolVersion1) { 186 | callback([[NSError alloc] initWithDomain:PTProtocolErrorDomain code:0 userInfo:nil], 0, 0, 0); 187 | } else { 188 | frame->type = ntohl(frame->type); 189 | frame->tag = ntohl(frame->tag); 190 | frame->payloadSize = ntohl(frame->payloadSize); 191 | callback(nil, frame->type, frame->tag, frame->payloadSize); 192 | } 193 | } 194 | }); 195 | } 196 | 197 | 198 | - (void)readPayloadOfSize:(size_t)payloadSize overChannel:(dispatch_io_t)channel callback:(void(^)(NSError *error, dispatch_data_t contiguousData, const uint8_t *buffer, size_t bufferSize))callback { 199 | __block dispatch_data_t allData = NULL; 200 | dispatch_io_read(channel, 0, payloadSize, queue_, ^(bool done, dispatch_data_t data, int error) { 201 | size_t dataSize = dispatch_data_get_size(data); 202 | 203 | if (dataSize) { 204 | if (!allData) { 205 | allData = data; 206 | } else { 207 | allData = dispatch_data_create_concat(allData, data); 208 | } 209 | } 210 | 211 | if (done) { 212 | if (error != 0) { 213 | callback([[NSError alloc] initWithDomain:NSPOSIXErrorDomain code:error userInfo:nil], NULL, NULL, 0); 214 | return; 215 | } 216 | 217 | if (dataSize == 0) { 218 | callback(nil, NULL, NULL, 0); 219 | return; 220 | } 221 | 222 | uint8_t *buffer = NULL; 223 | size_t bufferSize = 0; 224 | PT_PRECISE_LIFETIME dispatch_data_t contiguousData = NULL; 225 | 226 | if (allData) { 227 | contiguousData = dispatch_data_create_map(allData, (const void **)&buffer, &bufferSize); 228 | if (!contiguousData) { 229 | callback([[NSError alloc] initWithDomain:NSPOSIXErrorDomain code:ENOMEM userInfo:nil], NULL, NULL, 0); 230 | return; 231 | } 232 | } 233 | 234 | callback(nil, contiguousData, buffer, bufferSize); 235 | } 236 | }); 237 | } 238 | 239 | 240 | - (void)readAndDiscardDataOfSize:(size_t)size overChannel:(dispatch_io_t)channel callback:(void(^)(NSError*, BOOL))callback { 241 | dispatch_io_read(channel, 0, size, queue_, ^(bool done, dispatch_data_t data, int error) { 242 | if (done && callback) { 243 | size_t dataSize = data ? dispatch_data_get_size(data) : 0; 244 | callback(error == 0 ? nil : [[NSError alloc] initWithDomain:NSPOSIXErrorDomain code:error userInfo:nil], dataSize == 0); 245 | } 246 | }); 247 | } 248 | 249 | 250 | - (void)readFramesOverChannel:(dispatch_io_t)channel onFrame:(void(^)(NSError*, uint32_t, uint32_t, uint32_t, dispatch_block_t))onFrame { 251 | [self readFrameOverChannel:channel callback:^(NSError *error, uint32_t type, uint32_t tag, uint32_t payloadSize) { 252 | onFrame(error, type, tag, payloadSize, ^{ 253 | if (type != PTFrameTypeEndOfStream) { 254 | [self readFramesOverChannel:channel onFrame:onFrame]; 255 | } 256 | }); 257 | }]; 258 | } 259 | 260 | 261 | @end 262 | 263 | 264 | @interface _PTDispatchData : NSObject { 265 | dispatch_data_t dispatchData_; 266 | } 267 | @end 268 | @implementation _PTDispatchData 269 | - (id)initWithDispatchData:(dispatch_data_t)dispatchData { 270 | if (!(self = [super init])) return nil; 271 | dispatchData_ = dispatchData; 272 | return self; 273 | } 274 | @end 275 | 276 | @implementation NSData (PTProtocol) 277 | 278 | #pragma clang diagnostic push 279 | #pragma clang diagnostic ignored "-Wunused-getter-return-value" 280 | 281 | - (dispatch_data_t)createReferencingDispatchData { 282 | // Note: The queue is used to submit the destructor. Since we only perform an 283 | // atomic release of self, it doesn't really matter which queue is used, thus 284 | // we use the current calling queue. 285 | return dispatch_data_create((const void*)self.bytes, self.length, dispatch_get_main_queue(), ^{ 286 | // trick to have the block capture the data, thus retain/releasing 287 | [self length]; 288 | }); 289 | } 290 | 291 | #pragma clang diagnostic pop 292 | 293 | + (NSData *)dataWithContentsOfDispatchData:(dispatch_data_t)data { 294 | if (!data) { 295 | return nil; 296 | } 297 | uint8_t *buffer = NULL; 298 | size_t bufferSize = 0; 299 | PT_PRECISE_LIFETIME dispatch_data_t contiguousData = dispatch_data_create_map(data, (const void **)&buffer, &bufferSize); 300 | if (!contiguousData) { 301 | return nil; 302 | } 303 | 304 | _PTDispatchData *dispatchDataRef = [[_PTDispatchData alloc] initWithDispatchData:contiguousData]; 305 | NSData *newData = [NSData dataWithBytesNoCopy:(void*)buffer length:bufferSize freeWhenDone:NO]; 306 | static const bool kDispatchDataRefKey; 307 | objc_setAssociatedObject(newData, (const void*)kDispatchDataRefKey, dispatchDataRef, OBJC_ASSOCIATION_RETAIN); 308 | 309 | return newData; 310 | } 311 | 312 | // Decode *data* as a peroperty list-encoded dictionary. Returns nil on failure. 313 | + (NSDictionary *)dictionaryWithContentsOfData:(NSData *)data { 314 | if (!data) { 315 | return nil; 316 | } 317 | return [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:NULL error:nil]; 318 | } 319 | 320 | @end 321 | 322 | 323 | @implementation NSDictionary (PTProtocol) 324 | 325 | - (dispatch_data_t)createReferencingDispatchData { 326 | NSError *error = nil; 327 | NSData *plistData = [NSPropertyListSerialization dataWithPropertyList:self format:NSPropertyListBinaryFormat_v1_0 options:0 error:&error]; 328 | if (!plistData) { 329 | NSLog(@"Failed to serialize property list: %@", error); 330 | return nil; 331 | } else { 332 | return [plistData createReferencingDispatchData]; 333 | } 334 | } 335 | @end 336 | -------------------------------------------------------------------------------- /Peertalk macOS Example/PTAppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "PTAppDelegate.h" 2 | 3 | #import "PTExampleProtocol.h" 4 | 5 | #import 6 | #import 7 | #import 8 | 9 | @interface PTAppDelegate () { 10 | // If the remote connection is over USB transport... 11 | NSNumber *connectingToDeviceID_; 12 | NSNumber *connectedDeviceID_; 13 | NSDictionary *connectedDeviceProperties_; 14 | NSDictionary *remoteDeviceInfo_; 15 | dispatch_queue_t notConnectedQueue_; 16 | BOOL notConnectedQueueSuspended_; 17 | PTChannel *connectedChannel_; 18 | NSDictionary *consoleTextAttributes_; 19 | NSDictionary *consoleStatusTextAttributes_; 20 | NSMutableDictionary *pings_; 21 | } 22 | 23 | @property (readonly) NSNumber *connectedDeviceID; 24 | @property PTChannel *connectedChannel; 25 | 26 | - (void)presentMessage:(NSString*)message isStatus:(BOOL)isStatus; 27 | - (void)startListeningForDevices; 28 | - (void)didDisconnectFromDevice:(NSNumber*)deviceID; 29 | - (void)disconnectFromCurrentChannel; 30 | - (void)enqueueConnectToLocalIPv4Port; 31 | - (void)connectToLocalIPv4Port; 32 | - (void)connectToUSBDevice; 33 | - (void)ping; 34 | 35 | @end 36 | 37 | 38 | @implementation PTAppDelegate 39 | 40 | @synthesize window = window_; 41 | @synthesize inputTextField = inputTextField_; 42 | @synthesize outputTextView = outputTextView_; 43 | @synthesize connectedDeviceID = connectedDeviceID_; 44 | 45 | 46 | - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { 47 | // We use a serial queue that we toggle depending on if we are connected or 48 | // not. When we are not connected to a peer, the queue is running to handle 49 | // "connect" tries. When we are connected to a peer, the queue is suspended 50 | // thus no longer trying to connect. 51 | notConnectedQueue_ = dispatch_queue_create("PTExample.notConnectedQueue", DISPATCH_QUEUE_SERIAL); 52 | 53 | // Configure the output NSTextView we use for UI feedback 54 | outputTextView_.textContainerInset = NSMakeSize(15.0, 10.0); 55 | consoleTextAttributes_ = [NSDictionary dictionaryWithObjectsAndKeys: 56 | [NSFont fontWithName:@"helvetica" size:16.0], NSFontAttributeName, 57 | [NSColor lightGrayColor], NSForegroundColorAttributeName, 58 | nil]; 59 | consoleStatusTextAttributes_ = [NSDictionary dictionaryWithObjectsAndKeys: 60 | [NSFont fontWithName:@"menlo" size:11.0], NSFontAttributeName, 61 | [NSColor darkGrayColor], NSForegroundColorAttributeName, 62 | nil]; 63 | 64 | // Configure the input NSTextField we use for UI input 65 | [inputTextField_ setFont:[NSFont fontWithDescriptor:[[consoleTextAttributes_ objectForKey:NSFontAttributeName] fontDescriptor] size:14.0]]; 66 | [self.window makeFirstResponder:inputTextField_]; 67 | 68 | // Start listening for device attached/detached notifications 69 | [self startListeningForDevices]; 70 | 71 | // Start trying to connect to local IPv4 port (defined in PTExampleProtocol.h) 72 | [self enqueueConnectToLocalIPv4Port]; 73 | 74 | // Put a little message in the UI 75 | [self presentMessage:@"Ready for action — connecting at will." isStatus:YES]; 76 | 77 | // Start pinging 78 | [self ping]; 79 | } 80 | 81 | 82 | - (IBAction)sendMessage:(id)sender { 83 | if (connectedChannel_) { 84 | NSString *message = self.inputTextField.stringValue; 85 | dispatch_data_t payload = PTExampleTextDispatchDataWithString(message); 86 | [connectedChannel_ sendFrameOfType:PTExampleFrameTypeTextMessage tag:PTFrameNoTag withPayload:(NSData *)payload callback:^(NSError *error) { 87 | if (error) { 88 | NSLog(@"Failed to send message: %@", error); 89 | } 90 | }]; 91 | [self presentMessage:[NSString stringWithFormat:@"[you]: %@", message] isStatus:NO]; 92 | self.inputTextField.stringValue = @""; 93 | } 94 | } 95 | 96 | 97 | - (void)presentMessage:(NSString*)message isStatus:(BOOL)isStatus { 98 | NSLog(@">> %@", message); 99 | [self.outputTextView.textStorage beginEditing]; 100 | if (self.outputTextView.textStorage.length > 0) { 101 | message = [@"\n" stringByAppendingString:message]; 102 | } 103 | [self.outputTextView.textStorage appendAttributedString:[[NSAttributedString alloc] initWithString:message attributes:isStatus ? consoleStatusTextAttributes_ : consoleTextAttributes_]]; 104 | [self.outputTextView.textStorage endEditing]; 105 | 106 | [NSAnimationContext beginGrouping]; 107 | [NSAnimationContext currentContext].duration = 0.15; 108 | [NSAnimationContext currentContext].timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; 109 | NSClipView* clipView = [[self.outputTextView enclosingScrollView] contentView]; 110 | NSRect clipViewBounds = clipView.bounds; 111 | NSPoint newOrigin = clipViewBounds.origin; 112 | newOrigin.y += 5.0; // hack A 1/2 113 | [clipView setBoundsOrigin:newOrigin]; // hack A 2/2 114 | newOrigin.y += 1000.0; 115 | clipViewBounds.origin = newOrigin; 116 | clipViewBounds = [clipView constrainBoundsRect:clipViewBounds]; 117 | [clipView.animator setBoundsOrigin:clipViewBounds.origin]; 118 | [NSAnimationContext endGrouping]; 119 | } 120 | 121 | 122 | - (PTChannel*)connectedChannel { 123 | return connectedChannel_; 124 | } 125 | 126 | - (void)setConnectedChannel:(PTChannel*)connectedChannel { 127 | connectedChannel_ = connectedChannel; 128 | 129 | // Toggle the notConnectedQueue_ depending on if we are connected or not 130 | if (!connectedChannel_ && notConnectedQueueSuspended_) { 131 | dispatch_resume(notConnectedQueue_); 132 | notConnectedQueueSuspended_ = NO; 133 | } else if (connectedChannel_ && !notConnectedQueueSuspended_) { 134 | dispatch_suspend(notConnectedQueue_); 135 | notConnectedQueueSuspended_ = YES; 136 | } 137 | 138 | if (!connectedChannel_ && connectingToDeviceID_) { 139 | [self enqueueConnectToUSBDevice]; 140 | } 141 | } 142 | 143 | 144 | #pragma mark - Ping 145 | 146 | 147 | - (void)pongWithTag:(uint32_t)tagno error:(NSError*)error { 148 | NSNumber *tag = [NSNumber numberWithUnsignedInt:tagno]; 149 | NSMutableDictionary *pingInfo = [pings_ objectForKey:tag]; 150 | if (pingInfo) { 151 | NSDate *now = [NSDate date]; 152 | [pingInfo setObject:now forKey:@"date ended"]; 153 | [pings_ removeObjectForKey:tag]; 154 | NSLog(@"Ping total roundtrip time: %.3f ms", [now timeIntervalSinceDate:[pingInfo objectForKey:@"date created"]]*1000.0); 155 | } 156 | } 157 | 158 | 159 | - (void)ping { 160 | if (connectedChannel_) { 161 | if (!pings_) { 162 | pings_ = [NSMutableDictionary dictionary]; 163 | } 164 | uint32_t tagno = [connectedChannel_.protocol newTag]; 165 | NSNumber *tag = [NSNumber numberWithUnsignedInt:tagno]; 166 | NSMutableDictionary *pingInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:[NSDate date], @"date created", nil]; 167 | [pings_ setObject:pingInfo forKey:tag]; 168 | [connectedChannel_ sendFrameOfType:PTExampleFrameTypePing tag:tagno withPayload:nil callback:^(NSError *error) { 169 | [self performSelector:@selector(ping) withObject:nil afterDelay:1.0]; 170 | [pingInfo setObject:[NSDate date] forKey:@"date sent"]; 171 | if (error) { 172 | [self->pings_ removeObjectForKey:tag]; 173 | } 174 | }]; 175 | } else { 176 | [self performSelector:@selector(ping) withObject:nil afterDelay:1.0]; 177 | } 178 | } 179 | 180 | 181 | #pragma mark - PTChannelDelegate 182 | 183 | 184 | - (BOOL)ioFrameChannel:(PTChannel*)channel shouldAcceptFrameOfType:(uint32_t)type tag:(uint32_t)tag payloadSize:(uint32_t)payloadSize { 185 | if ( type != PTExampleFrameTypeDeviceInfo 186 | && type != PTExampleFrameTypeTextMessage 187 | && type != PTExampleFrameTypePing 188 | && type != PTExampleFrameTypePong 189 | && type != PTFrameTypeEndOfStream) { 190 | NSLog(@"Unexpected frame of type %u", type); 191 | [channel close]; 192 | return NO; 193 | } else { 194 | return YES; 195 | } 196 | } 197 | 198 | - (void)ioFrameChannel:(PTChannel*)channel didReceiveFrameOfType:(uint32_t)type tag:(uint32_t)tag payload:(NSData *)payload { 199 | //NSLog(@"received %@, %u, %u, %@", channel, type, tag, payload); 200 | if (type == PTExampleFrameTypeDeviceInfo) { 201 | NSDictionary *deviceInfo = [NSData dictionaryWithContentsOfData:payload]; 202 | [self presentMessage:[NSString stringWithFormat:@"Connected to %@", deviceInfo.description] isStatus:YES]; 203 | } else if (type == PTExampleFrameTypeTextMessage) { 204 | PTExampleTextFrame *textFrame = (PTExampleTextFrame*)payload.bytes; 205 | textFrame->length = ntohl(textFrame->length); 206 | NSString *message = [[NSString alloc] initWithBytes:textFrame->utf8text length:textFrame->length encoding:NSUTF8StringEncoding]; 207 | [self presentMessage:[NSString stringWithFormat:@"[%@]: %@", channel.userInfo, message] isStatus:NO]; 208 | } else if (type == PTExampleFrameTypePong) { 209 | [self pongWithTag:tag error:nil]; 210 | } 211 | } 212 | 213 | - (void)ioFrameChannel:(PTChannel*)channel didEndWithError:(NSError*)error { 214 | if (connectedDeviceID_ && [connectedDeviceID_ isEqualToNumber:channel.userInfo]) { 215 | [self didDisconnectFromDevice:connectedDeviceID_]; 216 | } 217 | 218 | if (connectedChannel_ == channel) { 219 | [self presentMessage:[NSString stringWithFormat:@"Disconnected from %@", channel.userInfo] isStatus:YES]; 220 | self.connectedChannel = nil; 221 | } 222 | } 223 | 224 | 225 | #pragma mark - Wired device connections 226 | 227 | 228 | - (void)startListeningForDevices { 229 | NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 230 | 231 | [nc addObserverForName:PTUSBDeviceDidAttachNotification object:PTUSBHub.sharedHub queue:nil usingBlock:^(NSNotification *note) { 232 | NSNumber *deviceID = [note.userInfo objectForKey:PTUSBHubNotificationKeyDeviceID]; 233 | //NSLog(@"PTUSBDeviceDidAttachNotification: %@", note.userInfo); 234 | NSLog(@"PTUSBDeviceDidAttachNotification: %@", deviceID); 235 | 236 | dispatch_async(self->notConnectedQueue_, ^{ 237 | if (!self->connectingToDeviceID_ || ![deviceID isEqualToNumber:self->connectingToDeviceID_]) { 238 | [self disconnectFromCurrentChannel]; 239 | self->connectingToDeviceID_ = deviceID; 240 | self->connectedDeviceProperties_ = [note.userInfo objectForKey:PTUSBHubNotificationKeyProperties]; 241 | [self enqueueConnectToUSBDevice]; 242 | } 243 | }); 244 | }]; 245 | 246 | [nc addObserverForName:PTUSBDeviceDidDetachNotification object:PTUSBHub.sharedHub queue:nil usingBlock:^(NSNotification *note) { 247 | NSNumber *deviceID = [note.userInfo objectForKey:PTUSBHubNotificationKeyDeviceID]; 248 | //NSLog(@"PTUSBDeviceDidDetachNotification: %@", note.userInfo); 249 | NSLog(@"PTUSBDeviceDidDetachNotification: %@", deviceID); 250 | 251 | if ([self->connectingToDeviceID_ isEqualToNumber:deviceID]) { 252 | self->connectedDeviceProperties_ = nil; 253 | self->connectingToDeviceID_ = nil; 254 | if (self->connectedChannel_) { 255 | [self->connectedChannel_ close]; 256 | } 257 | } 258 | }]; 259 | } 260 | 261 | 262 | - (void)didDisconnectFromDevice:(NSNumber*)deviceID { 263 | NSLog(@"Disconnected from device"); 264 | if ([connectedDeviceID_ isEqualToNumber:deviceID]) { 265 | [self willChangeValueForKey:@"connectedDeviceID"]; 266 | connectedDeviceID_ = nil; 267 | [self didChangeValueForKey:@"connectedDeviceID"]; 268 | } 269 | } 270 | 271 | 272 | - (void)disconnectFromCurrentChannel { 273 | if (connectedDeviceID_ && connectedChannel_) { 274 | [connectedChannel_ close]; 275 | self.connectedChannel = nil; 276 | } 277 | } 278 | 279 | 280 | - (void)enqueueConnectToLocalIPv4Port { 281 | dispatch_async(notConnectedQueue_, ^{ 282 | dispatch_async(dispatch_get_main_queue(), ^{ 283 | [self connectToLocalIPv4Port]; 284 | }); 285 | }); 286 | } 287 | 288 | 289 | - (void)connectToLocalIPv4Port { 290 | PTChannel *channel = [PTChannel channelWithDelegate:self]; 291 | channel.userInfo = [NSString stringWithFormat:@"127.0.0.1:%d", PTExampleProtocolIPv4PortNumber]; 292 | [channel connectToPort:PTExampleProtocolIPv4PortNumber IPv4Address:INADDR_LOOPBACK callback:^(NSError *error, PTAddress *address) { 293 | if (error) { 294 | if (error.domain == NSPOSIXErrorDomain && (error.code == ECONNREFUSED || error.code == ETIMEDOUT)) { 295 | // this is an expected state 296 | } else { 297 | NSLog(@"Failed to connect to 127.0.0.1:%d: %@", PTExampleProtocolIPv4PortNumber, error); 298 | } 299 | } else { 300 | [self disconnectFromCurrentChannel]; 301 | self.connectedChannel = channel; 302 | channel.userInfo = address; 303 | NSLog(@"Connected to %@", address); 304 | } 305 | [self performSelector:@selector(enqueueConnectToLocalIPv4Port) withObject:nil afterDelay:PTAppReconnectDelay]; 306 | }]; 307 | } 308 | 309 | 310 | - (void)enqueueConnectToUSBDevice { 311 | dispatch_async(notConnectedQueue_, ^{ 312 | dispatch_async(dispatch_get_main_queue(), ^{ 313 | [self connectToUSBDevice]; 314 | }); 315 | }); 316 | } 317 | 318 | 319 | - (void)connectToUSBDevice { 320 | PTChannel *channel = [PTChannel channelWithDelegate:self]; 321 | channel.userInfo = connectingToDeviceID_; 322 | channel.delegate = self; 323 | 324 | [channel connectToPort:PTExampleProtocolIPv4PortNumber overUSBHub:PTUSBHub.sharedHub deviceID:connectingToDeviceID_ callback:^(NSError *error) { 325 | if (error) { 326 | if (error.domain == PTUSBHubErrorDomain && error.code == PTUSBHubErrorConnectionRefused) { 327 | NSLog(@"Failed to connect to device #%@: %@", channel.userInfo, error); 328 | } else { 329 | NSLog(@"Failed to connect to device #%@: %@", channel.userInfo, error); 330 | } 331 | if (channel.userInfo == self->connectingToDeviceID_) { 332 | [self performSelector:@selector(enqueueConnectToUSBDevice) withObject:nil afterDelay:PTAppReconnectDelay]; 333 | } 334 | } else { 335 | self->connectedDeviceID_ = self->connectingToDeviceID_; 336 | self.connectedChannel = channel; 337 | } 338 | }]; 339 | } 340 | 341 | @end 342 | -------------------------------------------------------------------------------- /peertalk-tests/PTProtocolTests.m: -------------------------------------------------------------------------------- 1 | #import "PTProtocolTests.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define PTAssertNotNULL(x) do { if ((x) == NULL) XCTFail(@"%s == NULL", #x); } while(0) 10 | 11 | static const uint32_t PTFrameTypeTestPing = UINT32_MAX - 1; 12 | static const uint32_t PTFrameTypeTestPingReply = PTFrameTypeTestPing - 1; 13 | 14 | @implementation PTProtocolTests 15 | 16 | - (void)setUp { 17 | [super setUp]; 18 | // Set-up code here. 19 | 20 | if (socketpair(AF_UNIX, SOCK_STREAM, 0, socket_) == -1) { 21 | XCTFail(@"socketpair"); 22 | } 23 | 24 | queue_[0] = dispatch_queue_create("PTProtocolTests.queue_[0]", DISPATCH_QUEUE_SERIAL); 25 | PTAssertNotNULL(queue_[0]); 26 | channel_[0] = dispatch_io_create(DISPATCH_IO_STREAM, socket_[0], queue_[0], ^(int error) { 27 | close(self->socket_[0]); 28 | }); 29 | PTAssertNotNULL(channel_[0]); 30 | 31 | queue_[1] = dispatch_queue_create("PTProtocolTests.queue_[1]", DISPATCH_QUEUE_SERIAL); 32 | PTAssertNotNULL(queue_[1]); 33 | channel_[1] = dispatch_io_create(DISPATCH_IO_STREAM, socket_[1], queue_[1], ^(int error) { 34 | close(self->socket_[1]); 35 | }); 36 | PTAssertNotNULL(channel_[1]); 37 | 38 | protocol_[0] = [[PTProtocol alloc] initWithDispatchQueue:queue_[0]]; 39 | protocol_[1] = [[PTProtocol alloc] initWithDispatchQueue:queue_[1]]; 40 | } 41 | 42 | - (void)tearDown { 43 | dispatch_io_close(channel_[0], DISPATCH_IO_STOP); 44 | dispatch_io_close(channel_[1], DISPATCH_IO_STOP); 45 | 46 | protocol_[0] = nil; 47 | protocol_[1] = nil; 48 | 49 | [super tearDown]; 50 | } 51 | 52 | #pragma mark - 53 | #pragma mark Helpers 54 | 55 | - (void)write:(dispatch_data_t)data callback:(void(^)(void))callback { 56 | dispatch_io_write(channel_[0], 0, data, queue_[0], ^(bool done, dispatch_data_t data, int error) { 57 | if (done) { 58 | XCTAssertEqual(error, (int)0, @"Expected error == 0"); 59 | callback(); 60 | } 61 | }); 62 | } 63 | 64 | 65 | - (void)readFromOffset:(off_t)offset length:(size_t)length callback:(void(^)(dispatch_data_t contiguousData, const uint8_t *data, size_t size))callback { 66 | __block dispatch_data_t allData = NULL; 67 | dispatch_io_read(channel_[1], offset, length, queue_[1], ^(bool done, dispatch_data_t data, int error) { 68 | //NSLog(@"dispatch_io_read: done=%d data=%p error=%d", done, data, error); 69 | if (data) { 70 | if (!allData) { 71 | allData = data; 72 | } else { 73 | allData = dispatch_data_create_concat(allData, data); 74 | } 75 | } 76 | 77 | if (done) { 78 | XCTAssertEqual(error, (int)0, @"Expected error == 0"); 79 | PTAssertNotNULL(allData); 80 | 81 | uint8_t *buffer = NULL; 82 | size_t bufferSize = 0; 83 | dispatch_data_t contiguousData = dispatch_data_create_map(allData, (const void **)&buffer, &bufferSize); 84 | PTAssertNotNULL(contiguousData); 85 | callback(contiguousData, buffer, bufferSize); 86 | } 87 | }); 88 | } 89 | 90 | 91 | - (void)waitForSemaphore:(dispatch_semaphore_t)sem milliseconds:(uint64_t)ms { 92 | if (dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, ms * 1000000LL)) != 0L) { 93 | XCTFail(@"Timeout in dispatch_semaphore_wait"); 94 | } 95 | } 96 | 97 | 98 | - (void)readFrameWithClient:(int)clientIndex 99 | expectedFrameType:(uint32_t)expectedFrameType 100 | expectedFrameTag:(uint32_t)expectedFrameTag 101 | expectedPayloadSize:(uint32_t)expectedPayloadSize 102 | callback:(void(^)(dispatch_data_t contiguousData, const uint8_t *buffer, size_t bufferSize))callback { 103 | [protocol_[clientIndex] readFrameOverChannel:channel_[clientIndex] callback:^(NSError *error, uint32_t receivedFrameType, uint32_t receivedFrameTag, uint32_t receivedPayloadSize) { 104 | if (error) XCTFail(@"readFrameOverChannel failed: %@", error); 105 | XCTAssertEqual(receivedFrameType, expectedFrameType); 106 | XCTAssertEqual(receivedFrameTag, expectedFrameTag); 107 | XCTAssertEqual(receivedPayloadSize, expectedPayloadSize); 108 | 109 | if (expectedPayloadSize != 0) { 110 | [self->protocol_[clientIndex] readPayloadOfSize:receivedPayloadSize overChannel:self->channel_[clientIndex] callback:^(NSError *error, dispatch_data_t contiguousData, const uint8_t *buffer, size_t bufferSize) { 111 | PTAssertNotNULL(contiguousData); 112 | PTAssertNotNULL(buffer); 113 | XCTAssertEqual((uint32_t)bufferSize, receivedPayloadSize); 114 | callback(contiguousData, buffer, bufferSize); 115 | }]; 116 | } else { 117 | callback(nil, nil, 0); 118 | } 119 | }]; 120 | } 121 | 122 | 123 | #pragma mark - 124 | #pragma mark Test cases 125 | 126 | 127 | - (void)test1_basic_data_exchange_to_verify_socket_pair { 128 | dispatch_semaphore_t sem1 = dispatch_semaphore_create(0); 129 | 130 | const char *testMessage = "HELLO"; 131 | size_t testMessageSize = strlen(testMessage); 132 | 133 | // Write 134 | char *testMessageBytes = strdup(testMessage); 135 | dispatch_data_t data = dispatch_data_create((const void*)testMessageBytes, testMessageSize, queue_[0], ^{ 136 | free(testMessageBytes); 137 | }); 138 | [self write:data callback:^{}]; 139 | 140 | // Read 141 | [self readFromOffset:0 length:testMessageSize callback:^(dispatch_data_t contiguousData, const uint8_t *data, size_t size) { 142 | if (memcmp((const void *)testMessage, (const void *)data, size) != 0) { 143 | XCTFail(@"Received data differs from sent data"); 144 | } 145 | dispatch_semaphore_signal(sem1); 146 | }]; 147 | 148 | [self waitForSemaphore:sem1 milliseconds:1000]; 149 | } 150 | 151 | 152 | - (void)test2_protocol_transmit_frame { 153 | dispatch_semaphore_t sem1 = dispatch_semaphore_create(0); 154 | 155 | uint32_t frameTag = PTFrameNoTag; 156 | uint32_t payloadSize = 0; 157 | 158 | [protocol_[0] sendFrameOfType:PTFrameTypeTestPing tag:frameTag withPayload:nil overChannel:channel_[0] callback:^(NSError *error) { 159 | if (error) XCTFail(@"sendFrameOfType failed: %@", error); 160 | }]; 161 | 162 | [protocol_[1] readFrameOverChannel:channel_[1] callback:^(NSError *error, uint32_t receivedFrameType, uint32_t receivedFrameTag, uint32_t receivedPayloadSize) { 163 | if (error) XCTFail(@"readFrameOverChannel failed: %@", error); 164 | XCTAssertEqual(receivedFrameType, PTFrameTypeTestPing); 165 | XCTAssertEqual(receivedFrameTag, frameTag); 166 | XCTAssertEqual(receivedPayloadSize, payloadSize); 167 | 168 | dispatch_semaphore_signal(sem1); 169 | }]; 170 | 171 | [self waitForSemaphore:sem1 milliseconds:1000]; 172 | } 173 | 174 | 175 | - (void)test3_protocol_echo_frame { 176 | dispatch_semaphore_t sem1 = dispatch_semaphore_create(0); 177 | 178 | uint32_t frameTag = [protocol_[0] newTag]; 179 | uint32_t payloadSize = 0; 180 | 181 | // Send frame on channel 0 182 | [protocol_[0] sendFrameOfType:PTFrameTypeTestPing tag:frameTag withPayload:nil overChannel:channel_[0] callback:^(NSError *error) { 183 | if (error) XCTFail(@"sendFrameOfType failed: %@", error); 184 | }]; 185 | 186 | // Read frame on channel 1 187 | [protocol_[1] readFrameOverChannel:channel_[1] callback:^(NSError *error, uint32_t receivedFrameType, uint32_t receivedFrameTag, uint32_t receivedPayloadSize) { 188 | if (error) XCTFail(@"readFrameOverChannel failed: %@", error); 189 | XCTAssertEqual(receivedFrameType, PTFrameTypeTestPing); 190 | XCTAssertEqual(receivedFrameTag, frameTag); 191 | XCTAssertEqual(receivedPayloadSize, payloadSize); 192 | 193 | // Reply on channel 1 194 | [self->protocol_[1] sendFrameOfType:PTFrameTypeTestPingReply tag:receivedFrameTag withPayload:nil overChannel:self->channel_[1] callback:^(NSError *error) { 195 | if (error) XCTFail(@"sendFrameOfType failed: %@", error); 196 | }]; 197 | }]; 198 | 199 | // Read reply on channel 0 (we expect a reply) 200 | [protocol_[0] readFrameOverChannel:channel_[0] callback:^(NSError *error, uint32_t receivedFrameType, uint32_t receivedFrameTag, uint32_t receivedPayloadSize) { 201 | if (error) XCTFail(@"readFrameOverChannel failed: %@", error); 202 | XCTAssertEqual(receivedFrameType, PTFrameTypeTestPingReply); 203 | XCTAssertEqual(receivedFrameTag, frameTag); 204 | XCTAssertEqual(receivedPayloadSize, payloadSize); 205 | // Test case complete 206 | dispatch_semaphore_signal(sem1); 207 | }]; 208 | 209 | [self waitForSemaphore:sem1 milliseconds:1000]; 210 | } 211 | 212 | 213 | - (void)test4_protocol_transmit_frame_with_payload { 214 | dispatch_semaphore_t sem1 = dispatch_semaphore_create(0); 215 | 216 | NSString *textMessage = @"¡HELLO! This is ünt unicoded méssage"; 217 | NSData *payloadData = [textMessage dataUsingEncoding:NSUTF8StringEncoding]; 218 | dispatch_data_t payload = [payloadData createReferencingDispatchData]; 219 | 220 | [protocol_[0] sendFrameOfType:PTFrameTypeTestPing tag:PTFrameNoTag withPayload:payload overChannel:channel_[0] callback:^(NSError *error) { 221 | if (error) XCTFail(@"sendFrameOfType failed: %@", error); 222 | }]; 223 | 224 | [self readFrameWithClient:1 expectedFrameType:PTFrameTypeTestPing expectedFrameTag:PTFrameNoTag expectedPayloadSize:(uint32_t)dispatch_data_get_size(payload) callback:^(dispatch_data_t contiguousData, const uint8_t *buffer, size_t bufferSize) { 225 | 226 | if (memcmp((const void *)payloadData.bytes, (const void *)buffer, bufferSize) != 0) { 227 | XCTFail(@"Received payload differs from sent payload"); 228 | } 229 | 230 | NSString *receivedTextMessage = [[NSString alloc] initWithBytes:buffer length:bufferSize encoding:NSUTF8StringEncoding]; 231 | if (![textMessage isEqualToString:receivedTextMessage]) { 232 | XCTFail(@"Received payload interpreted as UTF-8 text differs from sent text"); 233 | } 234 | //else NSLog(@"Received payload as UTF-8 string: \"%@\"", receivedTextMessage); 235 | 236 | dispatch_semaphore_signal(sem1); 237 | }]; 238 | 239 | [self waitForSemaphore:sem1 milliseconds:1000]; 240 | } 241 | 242 | 243 | - (void)test5_protocol_transmit_multiple_frames { 244 | dispatch_semaphore_t sem1 = dispatch_semaphore_create(0); 245 | 246 | const int totalNumberOfFrames = 20; 247 | uint32_t frameTypes[totalNumberOfFrames]; 248 | uint32_t tags[totalNumberOfFrames]; 249 | 250 | for (int i = 0; i < totalNumberOfFrames; ++i ) { 251 | frameTypes[i] = PTFrameTypeTestPing - i; // note: PTFrameTypeTest* are adjusted to UINT32_MAX, thus we subtract to avoid overflow 252 | tags[i] = [protocol_[0] newTag]; 253 | [protocol_[0] sendFrameOfType:frameTypes[i] tag:tags[i] withPayload:nil overChannel:channel_[0] callback:^(NSError *error) { 254 | if (error) XCTFail(@"sendFrameOfType failed: %@", error); 255 | }]; 256 | } 257 | 258 | // The following is safe (instead of using readFramesOverChannel:onFrame:) 259 | // since we know there are no payloads involved. 260 | for (int i = 0; i < totalNumberOfFrames; ++i ) { 261 | [self readFrameWithClient:1 expectedFrameType:frameTypes[i] expectedFrameTag:tags[i] expectedPayloadSize:0 callback:^(dispatch_data_t contiguousData, const uint8_t *buffer, size_t bufferSize) { 262 | dispatch_semaphore_signal(sem1); 263 | }]; 264 | } 265 | 266 | // Classic "join" pattern to wait for all reads to finish 267 | for (int i = 0; i < totalNumberOfFrames; ++i ) { 268 | [self waitForSemaphore:sem1 milliseconds:100]; 269 | } 270 | } 271 | 272 | 273 | - (void)test6_protocol_transmit_multiple_frames_with_payload { 274 | dispatch_semaphore_t sem1 = dispatch_semaphore_create(0); 275 | 276 | const int totalNumberOfFrames = 20; 277 | 278 | NSMutableArray *frameTypes = [NSMutableArray arrayWithCapacity:totalNumberOfFrames]; 279 | NSMutableArray *tags = [NSMutableArray arrayWithCapacity:totalNumberOfFrames]; 280 | NSMutableArray *payloadData = [NSMutableArray arrayWithCapacity:totalNumberOfFrames]; 281 | 282 | // Send all frames on channel 0 283 | for (int i = 0; i < totalNumberOfFrames; ++i ) { 284 | uint32_t frameType = PTFrameTypeTestPing - i; 285 | [frameTypes addObject:[NSNumber numberWithUnsignedInt:frameType]]; // note: PTFrameTypeTest* are adjusted to UINT32_MAX, thus we subtract to avoid overflow 286 | uint32_t tag = [protocol_[0] newTag]; 287 | [tags addObject:[NSNumber numberWithUnsignedInt:tag]]; 288 | 289 | dispatch_data_t payload = NULL; 290 | 291 | // Only include a payload for 2/3 of the frames 292 | if (i % 3 != 0) { 293 | [payloadData addObject:[[NSString stringWithFormat:@"Frame #%d", i] dataUsingEncoding:NSUTF8StringEncoding]]; 294 | payload = [[payloadData objectAtIndex:i] createReferencingDispatchData]; 295 | } else { 296 | [payloadData addObject:[NSNull null]]; 297 | } 298 | 299 | [protocol_[0] sendFrameOfType:frameType tag:tag withPayload:payload overChannel:channel_[0] callback:^(NSError *error) { 300 | if (error) XCTFail(@"sendFrameOfType failed: %@", error); 301 | }]; 302 | } 303 | 304 | // Read all frames on channel 1 305 | __block int read_i = 0; 306 | [protocol_[1] readFramesOverChannel:channel_[1] onFrame:^(NSError *error, uint32_t type, uint32_t tag, uint32_t payloadSize, dispatch_block_t resumeReadingFrames) { 307 | if (error) XCTFail(@"readFramesOverChannel failed: %@", error); 308 | 309 | uint32_t expectedType = [[frameTypes objectAtIndex:read_i] unsignedIntValue]; 310 | uint32_t expectedTag = [[tags objectAtIndex:read_i] unsignedIntValue]; 311 | NSData *expectedPayloadData = [payloadData objectAtIndex:read_i]; 312 | if (expectedPayloadData == (id)[NSNull null]) 313 | expectedPayloadData = nil; 314 | 315 | XCTAssertEqual(type, expectedType); 316 | XCTAssertEqual(tag, expectedTag); 317 | XCTAssertEqual(payloadSize, (uint32_t)(expectedPayloadData ? expectedPayloadData.length : 0)); 318 | 319 | dispatch_block_t cont = ^{ 320 | ++read_i; 321 | if (read_i < totalNumberOfFrames) { 322 | resumeReadingFrames(); 323 | } else { 324 | dispatch_semaphore_signal(sem1); 325 | } 326 | }; 327 | 328 | if (payloadSize) { 329 | [self->protocol_[1] readPayloadOfSize:payloadSize overChannel:self->channel_[1] callback:^(NSError *error, dispatch_data_t contiguousData, const uint8_t *buffer, size_t bufferSize) { 330 | PTAssertNotNULL(contiguousData); 331 | PTAssertNotNULL(buffer); 332 | XCTAssertEqual((uint32_t)bufferSize, payloadSize); 333 | 334 | if (memcmp((const void *)(expectedPayloadData.bytes), (const void *)buffer, bufferSize) != 0) { 335 | XCTFail(@"Received payload differs from sent payload"); 336 | } 337 | 338 | //NSLog(@"Received payload as UTF-8 string: \"%@\"", [[NSString alloc] initWithBytes:buffer length:bufferSize encoding:NSUTF8StringEncoding]); 339 | cont(); 340 | }]; 341 | } else { 342 | cont(); 343 | } 344 | }]; 345 | 346 | // Give each read 100ms to complete, or fail with timeout 347 | [self waitForSemaphore:sem1 milliseconds:totalNumberOfFrames * 100]; 348 | } 349 | 350 | 351 | @end 352 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PeerTalk 6 | 7 | 8 | 9 | 10 | 11 | 289 | 290 | 291 | 292 | 293 |

PeerTalk

294 |
295 |
296 | Fork me on GitHub 297 | 300 | 301 | 326 | 327 | 413 | 414 | -------------------------------------------------------------------------------- /peertalk/PTChannel.m: -------------------------------------------------------------------------------- 1 | #import "PTChannel.h" 2 | #import "PTPrivate.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #import 10 | 11 | // Read member of sockaddr_in without knowing the family 12 | #define PT_SOCKADDR_ACCESS(ss, member4, member6) \ 13 | (((ss)->ss_family == AF_INET) ? ( \ 14 | ((const struct sockaddr_in *)(ss))->member4 \ 15 | ) : ( \ 16 | ((const struct sockaddr_in6 *)(ss))->member6 \ 17 | )) 18 | 19 | // Connection state (storage: uint8_t) 20 | #define kConnStateNone 0 21 | #define kConnStateConnecting 1 22 | #define kConnStateConnected 2 23 | #define kConnStateListening 3 24 | 25 | // Delegate support optimization (storage: uint8_t) 26 | #define kDelegateFlagImplements_ioFrameChannel_shouldAcceptFrameOfType_tag_payloadSize 1 27 | #define kDelegateFlagImplements_ioFrameChannel_didEndWithError 2 28 | #define kDelegateFlagImplements_ioFrameChannel_didAcceptConnection_fromAddress 4 29 | 30 | 31 | #pragma mark - 32 | // Note: We are careful about the size of this struct as each connected peer 33 | // implies one allocation of this struct. 34 | @interface PTChannel () { 35 | dispatch_io_t dispatchObj_channel_; 36 | dispatch_source_t dispatchObj_source_; 37 | NSError *endError_; // 64 bit 38 | @public // here be hacks 39 | id delegate_; // 64 bit 40 | uint8_t delegateFlags_; // 8 bit 41 | @private 42 | uint8_t connState_; // 8 bit 43 | //char padding_[6]; // 48 bit -- only if allocation speed is important 44 | } 45 | - (id)initWithProtocol:(PTProtocol*)protocol delegate:(id)delegate; 46 | - (BOOL)acceptIncomingConnection:(dispatch_fd_t)serverSocketFD; 47 | @end 48 | static const uint8_t kUserInfoKey; 49 | 50 | #pragma mark - 51 | @interface PTAddress () { 52 | struct sockaddr_storage sockaddr_; 53 | } 54 | - (id)initWithSockaddr:(const struct sockaddr_storage*)addr; 55 | @end 56 | 57 | #pragma mark - 58 | @implementation PTChannel 59 | 60 | @synthesize protocol = protocol_; 61 | 62 | + (PTChannel*)channelWithDelegate:(id)delegate { 63 | return [[PTChannel alloc] initWithProtocol:nil delegate:delegate]; 64 | } 65 | 66 | 67 | - (id)initWithProtocol:(PTProtocol *)protocol delegate:(id)delegate { 68 | if (!(self = [super init])) return nil; 69 | protocol_ = protocol ? protocol : [PTProtocol sharedProtocolForQueue:dispatch_get_main_queue()]; 70 | self.delegate = delegate; 71 | return self; 72 | } 73 | 74 | 75 | - (id)initWithProtocol:(PTProtocol *)protocol { 76 | return [self initWithProtocol:protocol delegate:nil]; 77 | } 78 | 79 | 80 | - (id)init { 81 | return [self initWithProtocol:[PTProtocol sharedProtocolForQueue:dispatch_get_main_queue()] delegate:nil]; 82 | } 83 | 84 | - (BOOL)isConnected { 85 | return connState_ == kConnStateConnecting || connState_ == kConnStateConnected; 86 | } 87 | 88 | 89 | - (BOOL)isListening { 90 | return connState_ == kConnStateListening; 91 | } 92 | 93 | 94 | - (id)userInfo { 95 | return objc_getAssociatedObject(self, (void*)&kUserInfoKey); 96 | } 97 | 98 | - (void)setUserInfo:(id)userInfo { 99 | objc_setAssociatedObject(self, (const void*)&kUserInfoKey, userInfo, OBJC_ASSOCIATION_RETAIN); 100 | } 101 | 102 | 103 | - (void)setConnState:(char)connState { 104 | connState_ = connState; 105 | } 106 | 107 | 108 | - (void)setDispatchChannel:(dispatch_io_t)channel { 109 | assert(connState_ == kConnStateConnecting || connState_ == kConnStateConnected || connState_ == kConnStateNone); 110 | dispatch_io_t prevChannel = dispatchObj_channel_; 111 | if (prevChannel != channel) { 112 | dispatchObj_channel_ = channel; 113 | if (!dispatchObj_channel_ && !dispatchObj_source_) { 114 | connState_ = kConnStateNone; 115 | } 116 | } 117 | } 118 | 119 | 120 | - (void)setDispatchSource:(dispatch_source_t)source { 121 | assert(connState_ == kConnStateListening || connState_ == kConnStateNone); 122 | dispatch_source_t prevSource = dispatchObj_source_; 123 | if (prevSource != source) { 124 | dispatchObj_source_ = source; 125 | if (!dispatchObj_channel_ && !dispatchObj_source_) { 126 | connState_ = kConnStateNone; 127 | } 128 | } 129 | } 130 | 131 | 132 | - (id)delegate { 133 | return delegate_; 134 | } 135 | 136 | 137 | - (void)setDelegate:(id)delegate { 138 | delegate_ = delegate; 139 | delegateFlags_ = 0; 140 | if (!delegate_) { 141 | return; 142 | } 143 | 144 | if ([delegate respondsToSelector:@selector(ioFrameChannel:shouldAcceptFrameOfType:tag:payloadSize:)]) { 145 | delegateFlags_ |= kDelegateFlagImplements_ioFrameChannel_shouldAcceptFrameOfType_tag_payloadSize; 146 | } 147 | 148 | if (delegate_ && [delegate respondsToSelector:@selector(ioFrameChannel:didEndWithError:)]) { 149 | delegateFlags_ |= kDelegateFlagImplements_ioFrameChannel_didEndWithError; 150 | } 151 | 152 | if (delegate_ && [delegate respondsToSelector:@selector(ioFrameChannel:didAcceptConnection:fromAddress:)]) { 153 | delegateFlags_ |= kDelegateFlagImplements_ioFrameChannel_didAcceptConnection_fromAddress; 154 | } 155 | } 156 | 157 | 158 | #pragma mark - Connecting 159 | 160 | 161 | - (void)connectToPort:(int)port overUSBHub:(PTUSBHub*)usbHub deviceID:(NSNumber*)deviceID callback:(void(^)(NSError *error))callback { 162 | assert(protocol_ != NULL); 163 | if (connState_ != kConnStateNone) { 164 | if (callback) callback([NSError errorWithDomain:NSPOSIXErrorDomain code:EPERM userInfo:nil]); 165 | return; 166 | } 167 | connState_ = kConnStateConnecting; 168 | [usbHub connectToDevice:deviceID port:port onStart:^(NSError *err, dispatch_io_t dispatchChannel) { 169 | NSError *error = err; 170 | if (!error) { 171 | [self startReadingFromConnectedChannel:dispatchChannel error:&error]; 172 | } else { 173 | self->connState_ = kConnStateNone; 174 | } 175 | if (callback) callback(error); 176 | } onEnd:^(NSError *error) { 177 | if (self->delegateFlags_ & kDelegateFlagImplements_ioFrameChannel_didEndWithError) { 178 | [self->delegate_ ioFrameChannel:self didEndWithError:error]; 179 | } 180 | self->endError_ = nil; 181 | }]; 182 | } 183 | 184 | 185 | - (void)connectToPort:(in_port_t)port IPv4Address:(in_addr_t)address callback:(void(^)(NSError *error, PTAddress *address))callback { 186 | assert(protocol_ != NULL); 187 | if (connState_ != kConnStateNone) { 188 | if (callback) callback([NSError errorWithDomain:NSPOSIXErrorDomain code:EPERM userInfo:nil], nil); 189 | return; 190 | } 191 | connState_ = kConnStateConnecting; 192 | 193 | int error = 0; 194 | 195 | // Create socket 196 | dispatch_fd_t fd = socket(AF_INET, SOCK_STREAM, 0); 197 | if (fd == -1) { 198 | perror("socket(AF_INET, SOCK_STREAM, 0) failed"); 199 | error = errno; 200 | if (callback) callback([[NSError alloc] initWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil], nil); 201 | return; 202 | } 203 | 204 | // Connect socket 205 | struct sockaddr_in addr; 206 | bzero((char *)&addr, sizeof(addr)); 207 | 208 | addr.sin_len = sizeof(addr); 209 | addr.sin_family = AF_INET; 210 | addr.sin_port = htons(port); 211 | //addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); 212 | //addr.sin_addr.s_addr = htonl(INADDR_ANY); 213 | addr.sin_addr.s_addr = htonl(address); 214 | 215 | // prevent SIGPIPE 216 | int on = 1; 217 | setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &on, sizeof(on)); 218 | 219 | // int socket, const struct sockaddr *address, socklen_t address_len 220 | if (connect(fd, (const struct sockaddr *)&addr, addr.sin_len) == -1) { 221 | //perror("connect"); 222 | error = errno; 223 | close(fd); 224 | if (callback) callback([[NSError alloc] initWithDomain:NSPOSIXErrorDomain code:error userInfo:nil], nil); 225 | return; 226 | } 227 | 228 | dispatch_io_t dispatchChannel = dispatch_io_create(DISPATCH_IO_STREAM, fd, protocol_.queue, ^(int error) { 229 | close(fd); 230 | if (self->delegateFlags_ & kDelegateFlagImplements_ioFrameChannel_didEndWithError) { 231 | NSError *err = error == 0 ? self->endError_ : [[NSError alloc] initWithDomain:NSPOSIXErrorDomain code:error userInfo:nil]; 232 | [self->delegate_ ioFrameChannel:self didEndWithError:err]; 233 | self->endError_ = nil; 234 | } 235 | }); 236 | 237 | if (!dispatchChannel) { 238 | close(fd); 239 | if (callback) callback([[NSError alloc] initWithDomain:@"PTError" code:0 userInfo:nil], nil); 240 | return; 241 | } 242 | 243 | // Success 244 | NSError *err = nil; 245 | PTAddress *ptAddr = [[PTAddress alloc] initWithSockaddr:(struct sockaddr_storage*)&addr]; 246 | [self startReadingFromConnectedChannel:dispatchChannel error:&err]; 247 | if (callback) callback(err, ptAddr); 248 | } 249 | 250 | 251 | #pragma mark - Listening and serving 252 | 253 | 254 | - (void)listenOnPort:(in_port_t)port IPv4Address:(in_addr_t)address callback:(void(^)(NSError *error))callback { 255 | assert(dispatchObj_source_ == nil); 256 | 257 | // Create socket 258 | dispatch_fd_t fd = socket(AF_INET, SOCK_STREAM, 0); 259 | if (fd == -1) { 260 | if (callback) callback([NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]); 261 | return; 262 | } 263 | 264 | // Connect socket 265 | struct sockaddr_in addr; 266 | bzero((char *)&addr, sizeof(addr)); 267 | 268 | addr.sin_family = AF_INET; 269 | addr.sin_port = htons(port); 270 | //addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); 271 | //addr.sin_addr.s_addr = htonl(INADDR_ANY); 272 | addr.sin_addr.s_addr = htonl(address); 273 | 274 | socklen_t socklen = sizeof(addr); 275 | 276 | int on = 1; 277 | 278 | if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) { 279 | close(fd); 280 | if (callback) callback([NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]); 281 | return; 282 | } 283 | 284 | if (fcntl(fd, F_SETFL, O_NONBLOCK) == -1) { 285 | close(fd); 286 | if (callback) callback([NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]); 287 | return; 288 | } 289 | 290 | if (bind(fd, (struct sockaddr*)&addr, socklen) != 0) { 291 | close(fd); 292 | if (callback) callback([NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]); 293 | return; 294 | } 295 | 296 | if (listen(fd, 512) != 0) { 297 | close(fd); 298 | if (callback) callback([NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]); 299 | return; 300 | } 301 | 302 | [self setDispatchSource:dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fd, 0, protocol_.queue)]; 303 | 304 | dispatch_source_set_event_handler(dispatchObj_source_, ^{ 305 | unsigned long nconns = dispatch_source_get_data(self->dispatchObj_source_); 306 | while ([self acceptIncomingConnection:fd] && --nconns); 307 | }); 308 | 309 | dispatch_source_set_cancel_handler(self->dispatchObj_source_, ^{ 310 | // Captures *self*, effectively holding a reference to *self* until cancelled. 311 | self->dispatchObj_source_ = nil; 312 | close(fd); 313 | if (self->delegateFlags_ & kDelegateFlagImplements_ioFrameChannel_didEndWithError) { 314 | [self->delegate_ ioFrameChannel:self didEndWithError:self->endError_]; 315 | self->endError_ = nil; 316 | } 317 | }); 318 | 319 | dispatch_resume(dispatchObj_source_); 320 | 321 | connState_ = kConnStateListening; 322 | if (callback) callback(nil); 323 | } 324 | 325 | 326 | - (BOOL)acceptIncomingConnection:(dispatch_fd_t)serverSocketFD { 327 | struct sockaddr_in addr; 328 | socklen_t addrLen = sizeof(addr); 329 | dispatch_fd_t clientSocketFD = accept(serverSocketFD, (struct sockaddr*)&addr, &addrLen); 330 | 331 | if (clientSocketFD == -1) { 332 | perror("accept()"); 333 | return NO; 334 | } 335 | 336 | // prevent SIGPIPE 337 | int on = 1; 338 | setsockopt(clientSocketFD, SOL_SOCKET, SO_NOSIGPIPE, &on, sizeof(on)); 339 | 340 | if (fcntl(clientSocketFD, F_SETFL, O_NONBLOCK) == -1) { 341 | perror("fcntl(.. O_NONBLOCK)"); 342 | close(clientSocketFD); 343 | return NO; 344 | } 345 | 346 | if (delegateFlags_ & kDelegateFlagImplements_ioFrameChannel_didAcceptConnection_fromAddress) { 347 | PTChannel *peerChannel = [[PTChannel alloc] initWithProtocol:protocol_ delegate:delegate_]; 348 | __block PTChannel *localChannelRef = self; 349 | dispatch_io_t dispatchChannel = dispatch_io_create(DISPATCH_IO_STREAM, clientSocketFD, protocol_.queue, ^(int error) { 350 | // Important note: This block captures *self*, thus a reference is held to 351 | // *self* until the fd is truly closed. 352 | localChannelRef = nil; 353 | 354 | close(clientSocketFD); 355 | 356 | if (peerChannel->delegateFlags_ & kDelegateFlagImplements_ioFrameChannel_didEndWithError) { 357 | NSError *err = error == 0 ? peerChannel->endError_ : [[NSError alloc] initWithDomain:NSPOSIXErrorDomain code:error userInfo:nil]; 358 | [peerChannel->delegate_ ioFrameChannel:peerChannel didEndWithError:err]; 359 | peerChannel->endError_ = nil; 360 | } 361 | }); 362 | 363 | [peerChannel setConnState:kConnStateConnected]; 364 | [peerChannel setDispatchChannel:dispatchChannel]; 365 | 366 | assert(((struct sockaddr_storage*)&addr)->ss_len == addrLen); 367 | PTAddress *address = [[PTAddress alloc] initWithSockaddr:(struct sockaddr_storage*)&addr]; 368 | [delegate_ ioFrameChannel:self didAcceptConnection:peerChannel fromAddress:address]; 369 | 370 | NSError *err = nil; 371 | if (![peerChannel startReadingFromConnectedChannel:dispatchChannel error:&err]) { 372 | NSLog(@"startReadingFromConnectedChannel failed in accept: %@", err); 373 | } 374 | } else { 375 | close(clientSocketFD); 376 | } 377 | return YES; 378 | } 379 | 380 | 381 | #pragma mark - Closing the channel 382 | 383 | 384 | - (void)close { 385 | if ((connState_ == kConnStateConnecting || connState_ == kConnStateConnected) && dispatchObj_channel_) { 386 | dispatch_io_close(dispatchObj_channel_, DISPATCH_IO_STOP); 387 | [self setDispatchChannel:NULL]; 388 | } else if (connState_ == kConnStateListening && dispatchObj_source_) { 389 | dispatch_source_cancel(dispatchObj_source_); 390 | } 391 | } 392 | 393 | 394 | - (void)cancel { 395 | if ((connState_ == kConnStateConnecting || connState_ == kConnStateConnected) && dispatchObj_channel_) { 396 | dispatch_io_close(dispatchObj_channel_, 0); 397 | [self setDispatchChannel:NULL]; 398 | } else if (connState_ == kConnStateListening && dispatchObj_source_) { 399 | dispatch_source_cancel(dispatchObj_source_); 400 | } 401 | } 402 | 403 | 404 | #pragma mark - Reading 405 | 406 | 407 | - (BOOL)startReadingFromConnectedChannel:(dispatch_io_t)channel error:(__autoreleasing NSError**)error { 408 | if (connState_ != kConnStateNone && connState_ != kConnStateConnecting && connState_ != kConnStateConnected) { 409 | if (error) *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:EPERM userInfo:nil]; 410 | return NO; 411 | } 412 | 413 | if (dispatchObj_channel_ != channel) { 414 | [self close]; 415 | [self setDispatchChannel:channel]; 416 | } 417 | 418 | connState_ = kConnStateConnected; 419 | 420 | // helper 421 | BOOL(^handleError)(NSError*,BOOL) = ^BOOL(NSError *error, BOOL isEOS) { 422 | if (error) { 423 | //NSLog(@"Error while communicating: %@", error); 424 | self->endError_ = error; 425 | [self close]; 426 | return YES; 427 | } else if (isEOS) { 428 | [self cancel]; 429 | return YES; 430 | } 431 | return NO; 432 | }; 433 | 434 | [protocol_ readFramesOverChannel:channel onFrame:^(NSError *error, uint32_t type, uint32_t tag, uint32_t payloadSize, dispatch_block_t resumeReadingFrames) { 435 | if (handleError(error, type == PTFrameTypeEndOfStream)) { 436 | return; 437 | } 438 | 439 | BOOL accepted = (channel == self->dispatchObj_channel_); 440 | if (accepted && (self->delegateFlags_ & kDelegateFlagImplements_ioFrameChannel_shouldAcceptFrameOfType_tag_payloadSize)) { 441 | accepted = [self->delegate_ ioFrameChannel:self shouldAcceptFrameOfType:type tag:tag payloadSize:payloadSize]; 442 | } 443 | 444 | if (payloadSize == 0) { 445 | if (accepted && self->delegate_) { 446 | [self->delegate_ ioFrameChannel:self didReceiveFrameOfType:type tag:tag payload:nil]; 447 | } else { 448 | // simply ignore the frame 449 | } 450 | resumeReadingFrames(); 451 | } else { 452 | // has payload 453 | if (!accepted) { 454 | // Read and discard payload, ignoring frame 455 | [self->protocol_ readAndDiscardDataOfSize:payloadSize overChannel:channel callback:^(NSError *error, BOOL endOfStream) { 456 | if (!handleError(error, endOfStream)) { 457 | resumeReadingFrames(); 458 | } 459 | }]; 460 | } else { 461 | [self->protocol_ readPayloadOfSize:payloadSize overChannel:channel callback:^(NSError *error, dispatch_data_t contiguousData, const uint8_t *buffer, size_t bufferSize) { 462 | if (handleError(error, bufferSize == 0)) { 463 | return; 464 | } 465 | 466 | if (self->delegate_) { 467 | // dispatch_data_t can be cast to (NSData *) 468 | [self->delegate_ ioFrameChannel:self didReceiveFrameOfType:type tag:tag payload:(NSData *)contiguousData]; 469 | } 470 | 471 | resumeReadingFrames(); 472 | }]; 473 | } 474 | } 475 | }]; 476 | 477 | return YES; 478 | } 479 | 480 | 481 | #pragma mark - Sending 482 | 483 | - (void)sendFrameOfType:(uint32_t)frameType tag:(uint32_t)tag withPayload:(NSData *)payload callback:(void(^)(NSError *error))callback { 484 | if (connState_ == kConnStateConnecting || connState_ == kConnStateConnected) { 485 | dispatch_data_t payloadCopy = dispatch_data_create(payload.bytes, payload.length, NULL, DISPATCH_DATA_DESTRUCTOR_DEFAULT); 486 | [protocol_ sendFrameOfType:frameType tag:tag withPayload:payloadCopy overChannel:dispatchObj_channel_ callback:callback]; 487 | } else if (callback) { 488 | callback([NSError errorWithDomain:NSPOSIXErrorDomain code:EPERM userInfo:nil]); 489 | } 490 | } 491 | 492 | #pragma mark - NSObject 493 | 494 | - (NSString*)description { 495 | id userInfo = objc_getAssociatedObject(self, (void*)&kUserInfoKey); 496 | return [NSString stringWithFormat:@"", self, ( connState_ == kConnStateConnecting ? @"connecting" 497 | : connState_ == kConnStateConnected ? @"connected" 498 | : connState_ == kConnStateListening ? @"listening" 499 | : @"closed"), 500 | userInfo ? " " : "", userInfo ? userInfo : @""]; 501 | } 502 | 503 | 504 | @end 505 | 506 | 507 | #pragma mark - 508 | @implementation PTAddress 509 | 510 | - (id)initWithSockaddr:(const struct sockaddr_storage*)addr { 511 | if (!(self = [super init])) return nil; 512 | assert(addr); 513 | memcpy((void*)&sockaddr_, (const void*)addr, addr->ss_len); 514 | return self; 515 | } 516 | 517 | 518 | - (NSString*)name { 519 | if (sockaddr_.ss_len) { 520 | const void *sin_addr = NULL; 521 | size_t bufsize = 0; 522 | if (sockaddr_.ss_family == AF_INET6) { 523 | bufsize = INET6_ADDRSTRLEN; 524 | sin_addr = (const void *)&((const struct sockaddr_in6*)&sockaddr_)->sin6_addr; 525 | } else { 526 | bufsize = INET_ADDRSTRLEN; 527 | sin_addr = (const void *)&((const struct sockaddr_in*)&sockaddr_)->sin_addr; 528 | } 529 | char *buf = CFAllocatorAllocate(kCFAllocatorDefault, bufsize+1, 0); 530 | if (inet_ntop(sockaddr_.ss_family, sin_addr, buf, (unsigned int)bufsize-1) == NULL) { 531 | CFAllocatorDeallocate(kCFAllocatorDefault, buf); 532 | return nil; 533 | } 534 | return [[NSString alloc] initWithBytesNoCopy:(void*)buf length:strlen(buf) encoding:NSUTF8StringEncoding freeWhenDone:YES]; 535 | } else { 536 | return nil; 537 | } 538 | } 539 | 540 | 541 | - (NSInteger)port { 542 | if (sockaddr_.ss_len) { 543 | return ntohs(PT_SOCKADDR_ACCESS(&sockaddr_, sin_port, sin6_port)); 544 | } else { 545 | return 0; 546 | } 547 | } 548 | 549 | 550 | - (NSString*)description { 551 | if (sockaddr_.ss_len) { 552 | return [NSString stringWithFormat:@"%@:%u", self.name, (unsigned)self.port]; 553 | } else { 554 | return @"(?)"; 555 | } 556 | } 557 | 558 | @end 559 | -------------------------------------------------------------------------------- /peertalk/PTUSBHub.m: -------------------------------------------------------------------------------- 1 | #import "PTUSBHub.h" 2 | #import "PTPrivate.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | NSString * const PTUSBHubErrorDomain = @"PTUSBHubError"; 11 | 12 | typedef uint32_t USBMuxPacketType; 13 | enum { 14 | USBMuxPacketTypeResult = 1, 15 | USBMuxPacketTypeConnect = 2, 16 | USBMuxPacketTypeListen = 3, 17 | USBMuxPacketTypeDeviceAdd = 4, 18 | USBMuxPacketTypeDeviceRemove = 5, 19 | // ? = 6, 20 | // ? = 7, 21 | USBMuxPacketTypePlistPayload = 8, 22 | }; 23 | 24 | typedef uint32_t USBMuxPacketProtocol; 25 | enum { 26 | USBMuxPacketProtocolBinary = 0, 27 | USBMuxPacketProtocolPlist = 1, 28 | }; 29 | 30 | typedef uint32_t USBMuxReplyCode; 31 | enum { 32 | USBMuxReplyCodeOK = 0, 33 | USBMuxReplyCodeBadCommand = 1, 34 | USBMuxReplyCodeBadDevice = 2, 35 | USBMuxReplyCodeConnectionRefused = 3, 36 | // ? = 4, 37 | // ? = 5, 38 | USBMuxReplyCodeBadVersion = 6, 39 | }; 40 | 41 | 42 | typedef struct usbmux_packet { 43 | uint32_t size; 44 | USBMuxPacketProtocol protocol; 45 | USBMuxPacketType type; 46 | uint32_t tag; 47 | char data[0]; 48 | } __attribute__((__packed__)) usbmux_packet_t; 49 | 50 | static const uint32_t kUsbmuxPacketMaxPayloadSize = UINT32_MAX - (uint32_t)sizeof(usbmux_packet_t); 51 | 52 | 53 | static uint32_t usbmux_packet_payload_size(usbmux_packet_t *upacket) { 54 | return upacket->size - sizeof(usbmux_packet_t); 55 | } 56 | 57 | 58 | static void *usbmux_packet_payload(usbmux_packet_t *upacket) { 59 | return (void*)upacket->data; 60 | } 61 | 62 | 63 | static void usbmux_packet_set_payload(usbmux_packet_t *upacket, 64 | const void *payload, 65 | uint32_t payloadLength) 66 | { 67 | memcpy(usbmux_packet_payload(upacket), payload, payloadLength); 68 | } 69 | 70 | 71 | static usbmux_packet_t *usbmux_packet_alloc(uint32_t payloadSize) { 72 | assert(payloadSize <= kUsbmuxPacketMaxPayloadSize); 73 | uint32_t upacketSize = sizeof(usbmux_packet_t) + payloadSize; 74 | usbmux_packet_t *upacket = CFAllocatorAllocate(kCFAllocatorDefault, upacketSize, 0); 75 | memset(upacket, 0, sizeof(usbmux_packet_t)); 76 | upacket->size = upacketSize; 77 | return upacket; 78 | } 79 | 80 | 81 | static usbmux_packet_t *usbmux_packet_create(USBMuxPacketProtocol protocol, 82 | USBMuxPacketType type, 83 | uint32_t tag, 84 | const void *payload, 85 | uint32_t payloadSize) 86 | { 87 | usbmux_packet_t *upacket = usbmux_packet_alloc(payloadSize); 88 | if (!upacket) { 89 | return NULL; 90 | } 91 | 92 | upacket->protocol = protocol; 93 | upacket->type = type; 94 | upacket->tag = tag; 95 | 96 | if (payload && payloadSize) { 97 | usbmux_packet_set_payload(upacket, payload, (uint32_t)payloadSize); 98 | } 99 | 100 | return upacket; 101 | } 102 | 103 | 104 | static void usbmux_packet_free(usbmux_packet_t *upacket) { 105 | CFAllocatorDeallocate(kCFAllocatorDefault, upacket); 106 | } 107 | 108 | 109 | NSNotificationName const PTUSBDeviceDidAttachNotification = @"PTUSBDeviceDidAttachNotification"; 110 | NSNotificationName const PTUSBDeviceDidDetachNotification = @"PTUSBDeviceDidDetachNotification"; 111 | 112 | PTUSBHubNotificationKey const PTUSBHubNotificationKeyDeviceID = @"DeviceID"; 113 | PTUSBHubNotificationKey const PTUSBHubNotificationKeyMessageType = @"MessageType"; 114 | PTUSBHubNotificationKey const PTUSBHubNotificationKeyProperties = @"Properties"; 115 | 116 | static NSString *kPlistPacketTypeListen = @"Listen"; 117 | static NSString *kPlistPacketTypeConnect = @"Connect"; 118 | 119 | // Represents a channel of communication between the host process and a remote 120 | // (device) system. In practice, a PTUSBChannel is connected to a usbmuxd 121 | // endpoint which is configured to either listen for device changes (the 122 | // PTUSBHub's channel is usually configured as a device notification listener) or 123 | // configured as a TCP bridge (e.g. channels returned from PTUSBHub's 124 | // connectToDevice:port:callback:). You should not create channels yourself, but 125 | // let PTUSBHub provide you with already configured channels. 126 | @interface PTUSBChannel : NSObject { 127 | dispatch_io_t channel_; 128 | dispatch_queue_t queue_; 129 | uint32_t nextPacketTag_; 130 | NSMutableDictionary *responseQueue_; 131 | BOOL autoReadPackets_; 132 | BOOL isReadingPackets_; 133 | } 134 | 135 | // The underlying dispatch I/O channel. This is handy if you want to handle your 136 | // own I/O logic without PTUSBChannel. Remember to dispatch_retain() the channel 137 | // if you plan on using it as it might be released from the PTUSBChannel at any 138 | // point in time. 139 | @property (readonly) dispatch_io_t dispatchChannel; 140 | 141 | // The underlying file descriptor. 142 | @property (readonly) dispatch_fd_t fileDescriptor; 143 | 144 | // Send data 145 | - (void)sendDispatchData:(dispatch_data_t)data callback:(void(^)(NSError*))callback; 146 | - (void)sendData:(NSData*)data callback:(void(^)(NSError*))callback; 147 | 148 | // Read data 149 | - (void)readFromOffset:(off_t)offset length:(size_t)length callback:(void(^)(NSError *error, dispatch_data_t data))callback; 150 | 151 | // Close the channel, preventing further reads and writes, but letting currently 152 | // queued reads and writes finish. 153 | - (void)cancel; 154 | 155 | // Close the channel, preventing further reads and writes, immediately 156 | // terminating any ongoing reads and writes. 157 | - (void)stop; 158 | 159 | @end 160 | 161 | 162 | @interface PTUSBChannel (Private) 163 | 164 | + (NSDictionary*)packetDictionaryWithPacketType:(NSString*)messageType payload:(NSDictionary*)payload; 165 | - (BOOL)openOnQueue:(dispatch_queue_t)queue error:(NSError**)error onEnd:(void(^)(NSError *error))onEnd; 166 | - (void)listenWithBroadcastHandler:(void(^)(NSDictionary *packet))broadcastHandler callback:(void(^)(NSError*))callback; 167 | - (BOOL)errorFromPlistResponse:(NSDictionary*)packet error:(NSError**)error; 168 | - (uint32_t)nextPacketTag; 169 | - (void)sendPacketOfType:(USBMuxPacketType)type overProtocol:(USBMuxPacketProtocol)protocol tag:(uint32_t)tag payload:(NSData*)payload callback:(void(^)(NSError*))callback; 170 | - (void)sendPacket:(NSDictionary*)packet tag:(uint32_t)tag callback:(void(^)(NSError *error))callback; 171 | - (void)sendRequest:(NSDictionary*)packet callback:(void(^)(NSError *error, NSDictionary *responsePacket))callback; 172 | - (void)scheduleReadPacketWithCallback:(void(^)(NSError *error, NSDictionary *packet, uint32_t packetTag))callback; 173 | - (void)scheduleReadPacketWithBroadcastHandler:(void(^)(NSDictionary *packet))broadcastHandler; 174 | - (void)setNeedsReadingPacket; 175 | @end 176 | 177 | 178 | @interface PTUSBHub () { 179 | PTUSBChannel *channel_; 180 | } 181 | - (void)handleBroadcastPacket:(NSDictionary*)packet; 182 | @end 183 | 184 | 185 | @implementation PTUSBHub 186 | 187 | 188 | + (PTUSBHub*)sharedHub { 189 | static PTUSBHub *gSharedHub; 190 | static dispatch_once_t onceToken; 191 | dispatch_once(&onceToken, ^{ 192 | gSharedHub = [PTUSBHub new]; 193 | [gSharedHub listenOnQueue:dispatch_get_main_queue() onStart:^(NSError *error) { 194 | if (error) { 195 | NSLog(@"PTUSBHub failed to initialize: %@", error); 196 | } 197 | } onEnd:nil]; 198 | }); 199 | return gSharedHub; 200 | } 201 | 202 | 203 | - (id)init { 204 | if (!(self = [super init])) return nil; 205 | 206 | return self; 207 | } 208 | 209 | 210 | - (void)listenOnQueue:(dispatch_queue_t)queue onStart:(void(^)(NSError*))onStart onEnd:(void(^)(NSError*))onEnd { 211 | if (channel_) { 212 | if (onStart) onStart(nil); 213 | return; 214 | } 215 | channel_ = [PTUSBChannel new]; 216 | NSError *error = nil; 217 | if ([channel_ openOnQueue:queue error:&error onEnd:onEnd]) { 218 | [channel_ listenWithBroadcastHandler:^(NSDictionary *packet) { [self handleBroadcastPacket:packet]; } callback:onStart]; 219 | } else if (onStart) { 220 | onStart(error); 221 | } 222 | } 223 | 224 | 225 | - (void)connectToDevice:(NSNumber*)deviceID port:(int)port onStart:(void(^)(NSError*, dispatch_io_t))onStart onEnd:(void(^)(NSError*))onEnd { 226 | PTUSBChannel *channel = [PTUSBChannel new]; 227 | NSError *error = nil; 228 | 229 | if (![channel openOnQueue:dispatch_get_main_queue() error:&error onEnd:onEnd]) { 230 | onStart(error, nil); 231 | return; 232 | } 233 | 234 | port = ((port<<8) & 0xFF00) | (port>>8); // limit 235 | 236 | NSDictionary *packet = [PTUSBChannel packetDictionaryWithPacketType:kPlistPacketTypeConnect 237 | payload:[NSDictionary dictionaryWithObjectsAndKeys: 238 | deviceID, PTUSBHubNotificationKeyDeviceID, 239 | [NSNumber numberWithInt:port], @"PortNumber", 240 | nil]]; 241 | 242 | [channel sendRequest:packet callback:^(NSError *error_, NSDictionary *responsePacket) { 243 | NSError *error = error_; 244 | [channel errorFromPlistResponse:responsePacket error:&error]; 245 | onStart(error, (error ? nil : channel.dispatchChannel) ); 246 | }]; 247 | } 248 | 249 | 250 | - (void)handleBroadcastPacket:(NSDictionary*)packet { 251 | NSString *messageType = [packet objectForKey:PTUSBHubNotificationKeyMessageType]; 252 | 253 | if ([@"Attached" isEqualToString:messageType]) { 254 | [[NSNotificationCenter defaultCenter] postNotificationName:PTUSBDeviceDidAttachNotification object:self userInfo:packet]; 255 | } else if ([@"Detached" isEqualToString:messageType]) { 256 | [[NSNotificationCenter defaultCenter] postNotificationName:PTUSBDeviceDidDetachNotification object:self userInfo:packet]; 257 | } else { 258 | NSLog(@"Warning: Unhandled broadcast message: %@", packet); 259 | } 260 | } 261 | 262 | 263 | @end 264 | 265 | #pragma mark - 266 | 267 | @implementation PTUSBChannel 268 | 269 | + (NSDictionary*)packetDictionaryWithPacketType:(NSString*)messageType payload:(NSDictionary*)payload { 270 | NSDictionary *packet = nil; 271 | 272 | static NSString *bundleName = nil; 273 | static NSString *bundleVersion = nil; 274 | static dispatch_once_t onceToken; 275 | dispatch_once(&onceToken, ^{ 276 | NSDictionary *infoDict = [NSBundle mainBundle].infoDictionary; 277 | if (infoDict) { 278 | bundleName = [infoDict objectForKey:@"CFBundleName"]; 279 | bundleVersion = [[infoDict objectForKey:@"CFBundleVersion"] description]; 280 | } 281 | }); 282 | 283 | if (bundleName) { 284 | packet = [NSDictionary dictionaryWithObjectsAndKeys: 285 | messageType, PTUSBHubNotificationKeyMessageType, 286 | bundleName, @"ProgName", 287 | bundleVersion, @"ClientVersionString", 288 | nil]; 289 | } else { 290 | packet = [NSDictionary dictionaryWithObjectsAndKeys:messageType, PTUSBHubNotificationKeyMessageType, nil]; 291 | } 292 | 293 | if (payload) { 294 | NSMutableDictionary *mpacket = [NSMutableDictionary dictionaryWithDictionary:payload]; 295 | [mpacket addEntriesFromDictionary:packet]; 296 | packet = mpacket; 297 | } 298 | 299 | return packet; 300 | } 301 | 302 | 303 | - (id)init { 304 | if (!(self = [super init])) return nil; 305 | 306 | return self; 307 | } 308 | 309 | 310 | - (void)dealloc { 311 | if (channel_) { 312 | channel_ = nil; 313 | } 314 | } 315 | 316 | 317 | - (BOOL)valid { 318 | return !!channel_; 319 | } 320 | 321 | 322 | - (dispatch_io_t)dispatchChannel { 323 | return channel_; 324 | } 325 | 326 | 327 | - (dispatch_fd_t)fileDescriptor { 328 | return dispatch_io_get_descriptor(channel_); 329 | } 330 | 331 | 332 | - (BOOL)openOnQueue:(dispatch_queue_t)queue error:(NSError**)error onEnd:(void(^)(NSError*))onEnd { 333 | assert(queue != nil); 334 | assert(channel_ == nil); 335 | queue_ = queue; 336 | 337 | // Create socket 338 | dispatch_fd_t fd = socket(AF_UNIX, SOCK_STREAM, 0); 339 | if (fd == -1) { 340 | if (error) *error = [[NSError alloc] initWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]; 341 | return NO; 342 | } 343 | 344 | // prevent SIGPIPE 345 | int on = 1; 346 | setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &on, sizeof(on)); 347 | 348 | // Connect socket 349 | struct sockaddr_un addr; 350 | addr.sun_family = AF_UNIX; 351 | strcpy(addr.sun_path, "/private/var/run/usbmuxd"); 352 | socklen_t socklen = sizeof(addr); 353 | if (connect(fd, (struct sockaddr*)&addr, socklen) == -1) { 354 | if (error) *error = [[NSError alloc] initWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]; 355 | return NO; 356 | } 357 | 358 | channel_ = dispatch_io_create(DISPATCH_IO_STREAM, fd, queue_, ^(int error) { 359 | close(fd); 360 | if (onEnd) { 361 | onEnd(error == 0 ? nil : [[NSError alloc] initWithDomain:NSPOSIXErrorDomain code:error userInfo:nil]); 362 | } 363 | }); 364 | 365 | return YES; 366 | } 367 | 368 | 369 | - (void)listenWithBroadcastHandler:(void(^)(NSDictionary *packet))broadcastHandler callback:(void(^)(NSError*))callback { 370 | autoReadPackets_ = YES; 371 | [self scheduleReadPacketWithBroadcastHandler:broadcastHandler]; 372 | 373 | NSDictionary *packet = [PTUSBChannel packetDictionaryWithPacketType:kPlistPacketTypeListen payload:nil]; 374 | 375 | [self sendRequest:packet callback:^(NSError *error_, NSDictionary *responsePacket) { 376 | if (!callback) 377 | return; 378 | 379 | NSError *error = error_; 380 | [self errorFromPlistResponse:responsePacket error:&error]; 381 | 382 | callback(error); 383 | }]; 384 | } 385 | 386 | 387 | - (BOOL)errorFromPlistResponse:(NSDictionary*)packet error:(NSError**)error { 388 | if (!*error) { 389 | NSNumber *n = [packet objectForKey:@"Number"]; 390 | 391 | if (!n) { 392 | *error = [NSError errorWithDomain:PTUSBHubErrorDomain code:(n ? n.integerValue : 0) userInfo:nil]; 393 | return NO; 394 | } 395 | 396 | USBMuxReplyCode replyCode = (USBMuxReplyCode)n.integerValue; 397 | if (replyCode != 0) { 398 | NSString *errmessage = @"Unspecified error"; 399 | switch (replyCode) { 400 | case USBMuxReplyCodeBadCommand: errmessage = @"illegal command"; break; 401 | case USBMuxReplyCodeBadDevice: errmessage = @"unknown device"; break; 402 | case USBMuxReplyCodeConnectionRefused: errmessage = @"connection refused"; break; 403 | case USBMuxReplyCodeBadVersion: errmessage = @"invalid version"; break; 404 | default: break; 405 | } 406 | *error = [NSError errorWithDomain:PTUSBHubErrorDomain code:replyCode userInfo:[NSDictionary dictionaryWithObject:errmessage forKey:NSLocalizedDescriptionKey]]; 407 | return NO; 408 | } 409 | } 410 | return YES; 411 | } 412 | 413 | 414 | - (uint32_t)nextPacketTag { 415 | return ++nextPacketTag_; 416 | } 417 | 418 | 419 | - (void)sendRequest:(NSDictionary*)packet callback:(void(^)(NSError*, NSDictionary*))callback { 420 | uint32_t tag = [self nextPacketTag]; 421 | [self sendPacket:packet tag:tag callback:^(NSError *error) { 422 | if (error) { 423 | callback(error, nil); 424 | return; 425 | } 426 | // TODO: timeout un-triggered callbacks in responseQueue_ 427 | if (!self->responseQueue_) self->responseQueue_ = [NSMutableDictionary new]; 428 | [self->responseQueue_ setObject:callback forKey:[NSNumber numberWithUnsignedInt:tag]]; 429 | }]; 430 | 431 | // We are awaiting a response 432 | [self setNeedsReadingPacket]; 433 | } 434 | 435 | 436 | - (void)setNeedsReadingPacket { 437 | if (!isReadingPackets_) { 438 | [self scheduleReadPacketWithBroadcastHandler:nil]; 439 | } 440 | } 441 | 442 | 443 | - (void)scheduleReadPacketWithBroadcastHandler:(void(^)(NSDictionary *packet))broadcastHandler { 444 | assert(isReadingPackets_ == NO); 445 | 446 | [self scheduleReadPacketWithCallback:^(NSError *error, NSDictionary *packet, uint32_t packetTag) { 447 | // Interpret the package we just received 448 | if (packetTag == 0) { 449 | // Broadcast message 450 | if (broadcastHandler) broadcastHandler(packet); 451 | } else if (self->responseQueue_) { 452 | // Reply 453 | NSNumber *key = [NSNumber numberWithUnsignedInt:packetTag]; 454 | void(^requestCallback)(NSError*,NSDictionary*) = [self->responseQueue_ objectForKey:key]; 455 | if (requestCallback) { 456 | [self->responseQueue_ removeObjectForKey:key]; 457 | requestCallback(error, packet); 458 | } else { 459 | NSLog(@"Warning: Ignoring reply packet for which there is no registered callback. Packet => %@", packet); 460 | } 461 | } 462 | 463 | // Schedule reading another incoming package 464 | if (self->autoReadPackets_) { 465 | [self scheduleReadPacketWithBroadcastHandler:broadcastHandler]; 466 | } 467 | }]; 468 | } 469 | 470 | 471 | - (void)scheduleReadPacketWithCallback:(void(^)(NSError*, NSDictionary*, uint32_t))callback { 472 | static usbmux_packet_t ref_upacket; 473 | isReadingPackets_ = YES; 474 | 475 | // Read the first `sizeof(ref_upacket.size)` bytes off the channel_ 476 | dispatch_io_read(channel_, 0, sizeof(ref_upacket.size), queue_, ^(bool done, dispatch_data_t data, int error) { 477 | if (!done) 478 | return; 479 | 480 | if (error) { 481 | self->isReadingPackets_ = NO; 482 | callback([[NSError alloc] initWithDomain:NSPOSIXErrorDomain code:error userInfo:nil], nil, 0); 483 | return; 484 | } 485 | 486 | // Read size of incoming usbmux_packet_t 487 | uint32_t upacket_len = 0; 488 | char *buffer = NULL; 489 | size_t buffer_size = 0; 490 | PT_PRECISE_LIFETIME_UNUSED dispatch_data_t map_data = dispatch_data_create_map(data, (const void **)&buffer, &buffer_size); // objc_precise_lifetime guarantees 'map_data' isn't released before memcpy has a chance to do its thing 491 | assert(buffer_size == sizeof(ref_upacket.size)); 492 | assert(sizeof(upacket_len) == sizeof(ref_upacket.size)); 493 | memcpy((void *)&(upacket_len), (const void *)buffer, buffer_size); 494 | 495 | // Allocate a new usbmux_packet_t for the expected size 496 | uint32_t payloadLength = upacket_len - (uint32_t)sizeof(usbmux_packet_t); 497 | usbmux_packet_t *upacket = usbmux_packet_alloc(payloadLength); 498 | 499 | // Read rest of the incoming usbmux_packet_t 500 | off_t offset = sizeof(ref_upacket.size); 501 | dispatch_io_read(self->channel_, offset, upacket->size - offset, self->queue_, ^(bool done, dispatch_data_t data, int error) { 502 | //NSLog(@"dispatch_io_read X,Y: done=%d data=%p error=%d", done, data, error); 503 | 504 | if (!done) { 505 | return; 506 | } 507 | 508 | self->isReadingPackets_ = NO; 509 | 510 | if (error) { 511 | callback([[NSError alloc] initWithDomain:NSPOSIXErrorDomain code:error userInfo:nil], nil, 0); 512 | usbmux_packet_free(upacket); 513 | return; 514 | } 515 | 516 | if (upacket_len > kUsbmuxPacketMaxPayloadSize) { 517 | callback( 518 | [[NSError alloc] initWithDomain:PTUSBHubErrorDomain code:1 userInfo:@{ 519 | NSLocalizedDescriptionKey:@"Received a packet that is too large"}], 520 | nil, 521 | 0 522 | ); 523 | usbmux_packet_free(upacket); 524 | return; 525 | } 526 | 527 | // Copy read bytes onto our usbmux_packet_t 528 | char *buffer = NULL; 529 | size_t buffer_size = 0; 530 | PT_PRECISE_LIFETIME_UNUSED dispatch_data_t map_data = dispatch_data_create_map(data, (const void **)&buffer, &buffer_size); 531 | assert(buffer_size == upacket->size - offset); 532 | memcpy(((void *)(upacket))+offset, (const void *)buffer, buffer_size); 533 | 534 | // We only support plist protocol 535 | if (upacket->protocol != USBMuxPacketProtocolPlist) { 536 | callback([[NSError alloc] initWithDomain:PTUSBHubErrorDomain code:0 userInfo:[NSDictionary dictionaryWithObject:@"Unexpected package protocol" forKey:NSLocalizedDescriptionKey]], nil, upacket->tag); 537 | usbmux_packet_free(upacket); 538 | return; 539 | } 540 | 541 | // Only one type of packet in the plist protocol 542 | if (upacket->type != USBMuxPacketTypePlistPayload) { 543 | callback([[NSError alloc] initWithDomain:PTUSBHubErrorDomain code:0 userInfo:[NSDictionary dictionaryWithObject:@"Unexpected package type" forKey:NSLocalizedDescriptionKey]], nil, upacket->tag); 544 | usbmux_packet_free(upacket); 545 | return; 546 | } 547 | 548 | // Try to decode any payload as plist 549 | NSError *err = nil; 550 | NSDictionary *dict = nil; 551 | if (usbmux_packet_payload_size(upacket)) { 552 | dict = [NSPropertyListSerialization propertyListWithData:[NSData dataWithBytesNoCopy:usbmux_packet_payload(upacket) length:usbmux_packet_payload_size(upacket) freeWhenDone:NO] options:NSPropertyListImmutable format:NULL error:&err]; 553 | } 554 | 555 | // Invoke callback 556 | callback(err, dict, upacket->tag); 557 | usbmux_packet_free(upacket); 558 | }); 559 | }); 560 | } 561 | 562 | 563 | - (void)sendPacketOfType:(USBMuxPacketType)type 564 | overProtocol:(USBMuxPacketProtocol)protocol 565 | tag:(uint32_t)tag 566 | payload:(NSData*)payload 567 | callback:(void(^)(NSError*))callback 568 | { 569 | assert(payload.length <= kUsbmuxPacketMaxPayloadSize); 570 | usbmux_packet_t *upacket = usbmux_packet_create( 571 | protocol, 572 | type, 573 | tag, 574 | payload ? payload.bytes : nil, 575 | (uint32_t)(payload ? payload.length : 0) 576 | ); 577 | dispatch_data_t data = dispatch_data_create((const void*)upacket, upacket->size, queue_, ^{ 578 | // Free packet when data is freed 579 | usbmux_packet_free(upacket); 580 | }); 581 | [self sendDispatchData:data callback:callback]; 582 | } 583 | 584 | 585 | - (void)sendPacket:(NSDictionary*)packet tag:(uint32_t)tag callback:(void(^)(NSError*))callback { 586 | NSError *error = nil; 587 | // NSPropertyListBinaryFormat_v1_0 588 | NSData *plistData = [NSPropertyListSerialization dataWithPropertyList:packet format:NSPropertyListXMLFormat_v1_0 options:0 error:&error]; 589 | if (!plistData) { 590 | callback(error); 591 | } else { 592 | [self sendPacketOfType:USBMuxPacketTypePlistPayload overProtocol:USBMuxPacketProtocolPlist tag:tag payload:plistData callback:callback]; 593 | } 594 | } 595 | 596 | 597 | - (void)sendDispatchData:(dispatch_data_t)data callback:(void(^)(NSError*))callback { 598 | off_t offset = 0; 599 | dispatch_io_write(channel_, offset, data, queue_, ^(bool done, dispatch_data_t data, int _errno) { 600 | //NSLog(@"dispatch_io_write: done=%d data=%p error=%d", done, data, error); 601 | if (!done) 602 | return; 603 | if (callback) { 604 | NSError *err = nil; 605 | if (_errno) err = [[NSError alloc] initWithDomain:NSPOSIXErrorDomain code:_errno userInfo:nil]; 606 | callback(err); 607 | } 608 | }); 609 | } 610 | 611 | #pragma clang diagnostic push 612 | #pragma clang diagnostic ignored "-Wunused-getter-return-value" 613 | 614 | - (void)sendData:(NSData*)data callback:(void(^)(NSError*))callback { 615 | dispatch_data_t ddata = dispatch_data_create((const void*)data.bytes, data.length, queue_, ^{ 616 | // trick to have the block capture and retain the data 617 | [data length]; 618 | }); 619 | [self sendDispatchData:ddata callback:callback]; 620 | } 621 | 622 | #pragma clang diagnostic pop 623 | 624 | - (void)readFromOffset:(off_t)offset length:(size_t)length callback:(void(^)(NSError *error, dispatch_data_t data))callback { 625 | dispatch_io_read(channel_, offset, length, queue_, ^(bool done, dispatch_data_t data, int _errno) { 626 | if (!done) 627 | return; 628 | 629 | NSError *error = nil; 630 | if (_errno != 0) { 631 | error = [[NSError alloc] initWithDomain:NSPOSIXErrorDomain code:_errno userInfo:nil]; 632 | } 633 | 634 | callback(error, data); 635 | }); 636 | } 637 | 638 | 639 | - (void)cancel { 640 | if (channel_) { 641 | dispatch_io_close(channel_, 0); 642 | } 643 | } 644 | 645 | 646 | - (void)stop { 647 | if (channel_) { 648 | dispatch_io_close(channel_, DISPATCH_IO_STOP); 649 | } 650 | } 651 | 652 | @end 653 | --------------------------------------------------------------------------------