appium-webdriveragent 16.2.2 → 16.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/PrivateHeaders/XCTest/XCSynthesizedEventRecord.h +3 -3
  3. package/PrivateHeaders/XCTest/XCUIApplication.h +1 -1
  4. package/PrivateHeaders/XCTest/XCUIDevice.h +0 -1
  5. package/PrivateHeaders/XCTest/XCUIElement.h +1 -1
  6. package/Scripts/build.sh +6 -0
  7. package/WebDriverAgent.xcodeproj/project.pbxproj +1482 -2
  8. package/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationApp_watchOS.xcscheme +76 -0
  9. package/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_watchOS.xcscheme +86 -0
  10. package/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentLib_watchOS.xcscheme +85 -0
  11. package/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner.xcscheme +1 -1
  12. package/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner_watchOS.xcscheme +128 -0
  13. package/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m +5 -0
  14. package/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m +2 -0
  15. package/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.m +1 -1
  16. package/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h +34 -0
  17. package/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m +76 -4
  18. package/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.h +1 -1
  19. package/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m +1 -1
  20. package/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m +1 -1
  21. package/WebDriverAgentLib/Categories/XCUIElement+FBTyping.m +11 -0
  22. package/WebDriverAgentLib/Commands/FBCustomCommands.m +55 -4
  23. package/WebDriverAgentLib/Commands/FBElementCommands.m +6 -5
  24. package/WebDriverAgentLib/Commands/FBOrientationCommands.m +1 -1
  25. package/WebDriverAgentLib/Commands/FBSessionCommands.m +29 -5
  26. package/WebDriverAgentLib/Info.plist +2 -2
  27. package/WebDriverAgentLib/Routing/FBTCPSocket.h +70 -0
  28. package/WebDriverAgentLib/Routing/FBTCPSocket.m +199 -0
  29. package/WebDriverAgentLib/Routing/FBWebServer.m +33 -3
  30. package/WebDriverAgentLib/Routing/WatchOS/FBWatchHTTPServer.h +68 -0
  31. package/WebDriverAgentLib/Routing/WatchOS/FBWatchHTTPServer.m +376 -0
  32. package/WebDriverAgentLib/Routing/WatchOS/RouteRequest.h +30 -0
  33. package/WebDriverAgentLib/Routing/WatchOS/RouteRequest.m +25 -0
  34. package/WebDriverAgentLib/Routing/WatchOS/RouteResponse.h +30 -0
  35. package/WebDriverAgentLib/Routing/WatchOS/RouteResponse.m +54 -0
  36. package/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.m +8 -0
  37. package/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.h +1 -1
  38. package/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.m +1 -1
  39. package/WebDriverAgentLib/Utilities/FBConfiguration.h +1 -1
  40. package/WebDriverAgentLib/Utilities/FBConfiguration.m +3 -3
  41. package/WebDriverAgentLib/Utilities/FBImageProcessor.m +7 -2
  42. package/WebDriverAgentLib/Utilities/FBMacros.h +14 -5
  43. package/WebDriverAgentLib/Utilities/FBMathUtils.h +1 -1
  44. package/WebDriverAgentLib/Utilities/FBMathUtils.m +1 -1
  45. package/WebDriverAgentLib/Utilities/FBPasteboard.h +1 -1
  46. package/WebDriverAgentLib/Utilities/FBPasteboard.m +1 -1
  47. package/WebDriverAgentLib/Utilities/FBSettingsHandler.m +2 -2
  48. package/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.h +1 -1
  49. package/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m +1 -1
  50. package/WebDriverAgentTests/IntegrationApp_watchOS/ContentView.swift +31 -0
  51. package/WebDriverAgentTests/IntegrationApp_watchOS/WatchSpikeApp.swift +18 -0
  52. package/WebDriverAgentTests/IntegrationTests/FBForceTouchTests.m +3 -0
  53. package/WebDriverAgentTests/IntegrationTests/FBPasteboardTests.m +3 -2
  54. package/WebDriverAgentTests/IntegrationTests_watchOS/WDAAlertIntegrationTests.swift +23 -0
  55. package/WebDriverAgentTests/IntegrationTests_watchOS/WDAAppLifecycleIntegrationTests.swift +57 -0
  56. package/WebDriverAgentTests/IntegrationTests_watchOS/WDAClickIntegrationTests.swift +31 -0
  57. package/WebDriverAgentTests/IntegrationTests_watchOS/WDADeviceIntegrationTests.swift +113 -0
  58. package/WebDriverAgentTests/IntegrationTests_watchOS/WDAElementAttributeIntegrationTests.swift +57 -0
  59. package/WebDriverAgentTests/IntegrationTests_watchOS/WDAFindIntegrationTests.swift +126 -0
  60. package/WebDriverAgentTests/IntegrationTests_watchOS/WDAScreenshotAndSourceIntegrationTests.swift +52 -0
  61. package/WebDriverAgentTests/IntegrationTests_watchOS/WDASessionIntegrationTests.swift +72 -0
  62. package/WebDriverAgentTests/IntegrationTests_watchOS/WDATypingIntegrationTests.swift +57 -0
  63. package/WebDriverAgentTests/IntegrationTests_watchOS/WDAUnknownCommandIntegrationTests.swift +24 -0
  64. package/WebDriverAgentTests/IntegrationTests_watchOS/WDAWatchHTTPClient.swift +95 -0
  65. package/WebDriverAgentTests/IntegrationTests_watchOS/WDAWatchIntegrationTestCase.swift +61 -0
  66. package/package.json +1 -1
@@ -8,6 +8,9 @@
8
8
 
9
9
  #import "FBCustomCommands.h"
10
10
 
11
+ #if TARGET_OS_WATCH
12
+ @import WatchKit;
13
+ #endif
11
14
  #import <XCTest/XCUIDevice.h>
12
15
  #import <CoreLocation/CoreLocation.h>
13
16
 
@@ -52,7 +55,7 @@
52
55
  [[FBRoute GET:@"/wda/screen"].withoutSession respondWithTarget:self action:@selector(handleGetScreen:)],
53
56
  [[FBRoute GET:@"/wda/activeAppInfo"] respondWithTarget:self action:@selector(handleActiveAppInfo:)],
54
57
  [[FBRoute GET:@"/wda/activeAppInfo"].withoutSession respondWithTarget:self action:@selector(handleActiveAppInfo:)],
55
- #if !TARGET_OS_TV // tvOS does not provide relevant APIs
58
+ #if !TARGET_OS_TV && !TARGET_OS_WATCH // tvOS/watchOS do not provide relevant APIs
56
59
  [[FBRoute POST:@"/wda/setPasteboard"] respondWithTarget:self action:@selector(handleSetPasteboard:)],
57
60
  [[FBRoute POST:@"/wda/setPasteboard"].withoutSession respondWithTarget:self action:@selector(handleSetPasteboard:)],
58
61
  [[FBRoute POST:@"/wda/getPasteboard"] respondWithTarget:self action:@selector(handleGetPasteboard:)],
@@ -60,6 +63,10 @@
60
63
  [[FBRoute GET:@"/wda/batteryInfo"] respondWithTarget:self action:@selector(handleGetBatteryInfo:)],
61
64
  #endif
62
65
  [[FBRoute POST:@"/wda/pressButton"] respondWithTarget:self action:@selector(handlePressButtonCommand:)],
66
+ #if TARGET_OS_WATCH
67
+ [[FBRoute POST:@"/wda/rotateDigitalCrown"] respondWithTarget:self action:@selector(handleRotateDigitalCrownCommand:)],
68
+ [[FBRoute POST:@"/wda/performHandGesture"] respondWithTarget:self action:@selector(handlePerformHandGestureCommand:)],
69
+ #endif
63
70
  [[FBRoute POST:@"/wda/performAccessibilityAudit"] respondWithTarget:self action:@selector(handlePerformAccessibilityAudit:)],
64
71
  [[FBRoute POST:@"/wda/performIoHidEvent"] respondWithTarget:self action:@selector(handlePeformIOHIDEvent:)],
65
72
  [[FBRoute POST:@"/wda/expectNotification"] respondWithTarget:self action:@selector(handleExpectNotification:)],
@@ -72,8 +79,10 @@
72
79
  [[FBRoute GET:@"/wda/device/location"] respondWithTarget:self action:@selector(handleGetLocation:)],
73
80
  [[FBRoute GET:@"/wda/device/location"].withoutSession respondWithTarget:self action:@selector(handleGetLocation:)],
74
81
  #if !TARGET_OS_TV // tvOS does not provide relevant APIs
82
+ #if !TARGET_OS_WATCH
75
83
  #if __clang_major__ >= 15
76
84
  [[FBRoute POST:@"/wda/element/:uuid/keyboardInput"] respondWithTarget:self action:@selector(handleKeyboardInput:)],
85
+ #endif
77
86
  #endif
78
87
  [[FBRoute GET:@"/wda/simulatedLocation"] respondWithTarget:self action:@selector(handleGetSimulatedLocation:)],
79
88
  [[FBRoute GET:@"/wda/simulatedLocation"].withoutSession respondWithTarget:self action:@selector(handleGetSimulatedLocation:)],
@@ -151,7 +160,7 @@
151
160
  XCUIElement *mainStatusBar = app.statusBars.allElementsBoundByIndex.firstObject;
152
161
  CGSize statusBarSize = (nil == mainStatusBar) ? CGSizeZero : mainStatusBar.frame.size;
153
162
 
154
- #if TARGET_OS_TV
163
+ #if TARGET_OS_TV || TARGET_OS_WATCH
155
164
  CGSize screenSize = app.frame.size;
156
165
  #else
157
166
  CGSize screenSize = FBAdjustDimensionsForApplication(app.wdFrame.size, app.interfaceOrientation);
@@ -240,7 +249,7 @@
240
249
  };
241
250
  }
242
251
 
243
- #if !TARGET_OS_TV
252
+ #if !TARGET_OS_TV && !TARGET_OS_WATCH
244
253
  + (id<FBResponsePayload>)handleSetPasteboard:(FBRouteRequest *)request
245
254
  {
246
255
  NSString *contentType = request.arguments[@"contentType"] ?: @"plaintext";
@@ -290,6 +299,38 @@
290
299
  return FBResponseWithOK();
291
300
  }
292
301
 
302
+ #if TARGET_OS_WATCH
303
+ + (id<FBResponsePayload>)handleRotateDigitalCrownCommand:(FBRouteRequest *)request
304
+ {
305
+ NSNumber *delta = request.arguments[@"delta"];
306
+ if (nil == delta) {
307
+ return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"'delta' argument is mandatory"
308
+ traceback:nil]);
309
+ }
310
+ NSError *error;
311
+ if (![XCUIDevice.sharedDevice fb_rotateDigitalCrown:delta.doubleValue
312
+ velocity:request.arguments[@"velocity"]
313
+ error:&error]) {
314
+ return FBResponseWithUnknownError(error);
315
+ }
316
+ return FBResponseWithOK();
317
+ }
318
+
319
+ + (id<FBResponsePayload>)handlePerformHandGestureCommand:(FBRouteRequest *)request
320
+ {
321
+ NSString *gestureName = request.arguments[@"name"];
322
+ if (nil == gestureName) {
323
+ return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"'name' argument is mandatory"
324
+ traceback:nil]);
325
+ }
326
+ NSError *error;
327
+ if (![XCUIDevice.sharedDevice fb_performHandGesture:gestureName error:&error]) {
328
+ return FBResponseWithUnknownError(error);
329
+ }
330
+ return FBResponseWithOK();
331
+ }
332
+ #endif
333
+
293
334
  + (id<FBResponsePayload>)handleActivateSiri:(FBRouteRequest *)request
294
335
  {
295
336
  NSError *error;
@@ -357,7 +398,9 @@
357
398
  [locationManager setDistanceFilter:kCLHeadingFilterNone];
358
399
  // Always return the best acurate location data
359
400
  [locationManager setDesiredAccuracy:kCLLocationAccuracyBest];
401
+ #if !TARGET_OS_WATCH
360
402
  [locationManager setPausesLocationUpdatesAutomatically:NO];
403
+ #endif
361
404
  [locationManager startUpdatingLocation];
362
405
 
363
406
  CLAuthorizationStatus authStatus;
@@ -438,11 +481,17 @@
438
481
  @{
439
482
  @"currentLocale": currentLocale,
440
483
  @"timeZone": self.timeZone,
484
+ #if TARGET_OS_WATCH
485
+ @"name": WKInterfaceDevice.currentDevice.name,
486
+ @"model": WKInterfaceDevice.currentDevice.model,
487
+ @"uuid": @"unknown",
488
+ #else
441
489
  @"name": UIDevice.currentDevice.name,
442
490
  @"model": UIDevice.currentDevice.model,
443
491
  @"uuid": [UIDevice.currentDevice.identifierForVendor UUIDString] ?: @"unknown",
444
492
  // https://developer.apple.com/documentation/uikit/uiuserinterfaceidiom?language=objc
445
493
  @"userInterfaceIdiom": @(UIDevice.currentDevice.userInterfaceIdiom),
494
+ #endif
446
495
  @"userInterfaceStyle": self.userInterfaceStyle,
447
496
  #if TARGET_OS_SIMULATOR
448
497
  @"isSimulator": @(YES),
@@ -473,6 +522,7 @@
473
522
  }
474
523
 
475
524
  static id userInterfaceStyle = nil;
525
+ #if !TARGET_OS_WATCH
476
526
  static dispatch_once_t styleOnceToken;
477
527
  dispatch_once(&styleOnceToken, ^{
478
528
  if ([UITraitCollection respondsToSelector:NSSelectorFromString(@"currentTraitCollection")]) {
@@ -482,6 +532,7 @@
482
532
  }
483
533
  }
484
534
  });
535
+ #endif
485
536
 
486
537
  if (nil == userInterfaceStyle) {
487
538
  return @"unsupported";
@@ -571,7 +622,7 @@
571
622
  return FBResponseWithOK();
572
623
  }
573
624
 
574
- #if __clang_major__ >= 15
625
+ #if __clang_major__ >= 15 && !TARGET_OS_WATCH
575
626
  + (id<FBResponsePayload>)handleKeyboardInput:(FBRouteRequest *)request
576
627
  {
577
628
  FBElementCache *elementCache = request.session.elementCache;
@@ -73,7 +73,8 @@
73
73
  #if TARGET_OS_TV
74
74
  [[FBRoute GET:@"/element/:uuid/attribute/focused"] respondWithTarget:self action:@selector(handleGetFocused:)],
75
75
  [[FBRoute POST:@"/wda/element/:uuid/focuse"] respondWithTarget:self action:@selector(handleFocuse:)],
76
- #else
76
+ #elif !TARGET_OS_WATCH
77
+ // Gesture synthesis isn't supported on watchOS - only /element/:uuid/click is available.
77
78
  [[FBRoute POST:@"/wda/element/:uuid/swipe"] respondWithTarget:self action:@selector(handleSwipe:)],
78
79
  [[FBRoute POST:@"/wda/swipe"] respondWithTarget:self action:@selector(handleSwipe:)],
79
80
 
@@ -205,7 +206,7 @@
205
206
  ? [value componentsJoinedByString:@""]
206
207
  : value;
207
208
  XCUIElementType elementType = [element elementType];
208
- #if !TARGET_OS_TV
209
+ #if !TARGET_OS_TV && !TARGET_OS_WATCH
209
210
  if (elementType == XCUIElementTypePickerWheel) {
210
211
  [element adjustToPickerWheelValue:textToType];
211
212
  return FBResponseWithOK();
@@ -239,7 +240,7 @@
239
240
  {
240
241
  FBElementCache *elementCache = request.session.elementCache;
241
242
  XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] checkStaleness:YES];
242
- #if TARGET_OS_IOS
243
+ #if TARGET_OS_IOS || TARGET_OS_WATCH
243
244
  [element tap];
244
245
  #elif TARGET_OS_TV
245
246
  NSError *error = nil;
@@ -534,7 +535,7 @@
534
535
  XCUIApplication *app = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
535
536
 
536
537
  CGRect frame = app.wdFrame;
537
- #if TARGET_OS_TV
538
+ #if TARGET_OS_TV || TARGET_OS_WATCH
538
539
  CGSize screenSize = frame.size;
539
540
  #else
540
541
  CGSize screenSize = FBAdjustDimensionsForApplication(frame.size, app.interfaceOrientation);
@@ -551,7 +552,7 @@
551
552
  XCUIApplication *app = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
552
553
 
553
554
  CGRect frame = app.wdFrame;
554
- #if TARGET_OS_TV
555
+ #if TARGET_OS_TV || TARGET_OS_WATCH
555
556
  CGSize screenSize = frame.size;
556
557
  #else
557
558
  CGSize screenSize = FBAdjustDimensionsForApplication(frame.size, app.interfaceOrientation);
@@ -29,7 +29,7 @@ const struct FBWDOrientationValues FBWDOrientationValues = {
29
29
  .portraitUpsideDown = @"UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN",
30
30
  };
31
31
 
32
- #if !TARGET_OS_TV
32
+ #if !TARGET_OS_TV && !TARGET_OS_WATCH
33
33
 
34
34
  @implementation FBOrientationCommands
35
35
 
@@ -8,6 +8,10 @@
8
8
 
9
9
  #import "FBSessionCommands.h"
10
10
 
11
+ #if TARGET_OS_WATCH
12
+ @import WatchKit;
13
+ #endif
14
+
11
15
  #import "FBCapabilities.h"
12
16
  #import "FBConfiguration.h"
13
17
  #import "FBExceptions.h"
@@ -189,6 +193,16 @@
189
193
  [buildInfo setObject:version forKey:@"version"];
190
194
  }
191
195
 
196
+ #if TARGET_OS_WATCH
197
+ NSString *osName = @"watchOS";
198
+ NSString *osVersion = WKInterfaceDevice.currentDevice.systemVersion;
199
+ NSString *deviceKind = @"watch";
200
+ #else
201
+ NSString *osName = [[UIDevice currentDevice] systemName];
202
+ NSString *osVersion = [[UIDevice currentDevice] systemVersion];
203
+ NSString *deviceKind = [self.class deviceNameByUserInterfaceIdiom:[UIDevice currentDevice].userInterfaceIdiom];
204
+ #endif
205
+
192
206
  return FBResponseWithObject(
193
207
  @{
194
208
  @"ready" : @YES,
@@ -196,20 +210,20 @@
196
210
  @"state" : @"success",
197
211
  @"os" :
198
212
  @{
199
- @"name" : [[UIDevice currentDevice] systemName],
200
- @"version" : [[UIDevice currentDevice] systemVersion],
213
+ @"name" : osName,
214
+ @"version" : osVersion,
201
215
  @"sdkVersion": FBSDKVersion() ?: @"unknown",
202
216
  @"testmanagerdVersion": @(FBTestmanagerdVersion()),
203
217
  },
204
218
  @"ios" :
205
219
  @{
206
- #if TARGET_OS_SIMULATOR
207
- @"simulatorVersion" : [[UIDevice currentDevice] systemVersion],
220
+ #if TARGET_OS_SIMULATOR && !TARGET_OS_WATCH
221
+ @"simulatorVersion" : osVersion,
208
222
  #endif
209
223
  @"ip" : [XCUIDevice sharedDevice].fb_wifiIPAddress ?: [NSNull null]
210
224
  },
211
225
  @"build" : buildInfo.copy,
212
- @"device": [self.class deviceNameByUserInterfaceIdiom:[UIDevice currentDevice].userInterfaceIdiom]
226
+ @"device": deviceKind
213
227
  }
214
228
  );
215
229
  }
@@ -431,6 +445,7 @@
431
445
  };
432
446
  }
433
447
 
448
+ #if !TARGET_OS_WATCH
434
449
  /*
435
450
  Return the device kind as lower case
436
451
  */
@@ -447,14 +462,23 @@
447
462
  return @"Unknown";
448
463
 
449
464
  }
465
+ #endif
450
466
 
451
467
  + (NSDictionary *)currentCapabilities
452
468
  {
469
+ #if TARGET_OS_WATCH
470
+ return
471
+ @{
472
+ @"device": @"watch",
473
+ @"sdkVersion": WKInterfaceDevice.currentDevice.systemVersion
474
+ };
475
+ #else
453
476
  return
454
477
  @{
455
478
  @"device": [self.class deviceNameByUserInterfaceIdiom:[UIDevice currentDevice].userInterfaceIdiom],
456
479
  @"sdkVersion": [[UIDevice currentDevice] systemVersion]
457
480
  };
481
+ #endif
458
482
  }
459
483
 
460
484
  +(nullable id<FBResponsePayload>)openDeepLink:(NSString *)initialUrl
@@ -15,11 +15,11 @@
15
15
  <key>CFBundlePackageType</key>
16
16
  <string>FMWK</string>
17
17
  <key>CFBundleShortVersionString</key>
18
- <string>16.2.2</string>
18
+ <string>16.4.0</string>
19
19
  <key>CFBundleSignature</key>
20
20
  <string>????</string>
21
21
  <key>CFBundleVersion</key>
22
- <string>16.2.2</string>
22
+ <string>16.4.0</string>
23
23
  <key>NSPrincipalClass</key>
24
24
  <string/>
25
25
  </dict>
@@ -6,10 +6,56 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
 
9
+ // TARGET_OS_WATCH must be defined before the #if below runs, or (on some older Xcode/SDK
10
+ // toolchains) it silently evaluates as undefined/false here despite being true for the rest of
11
+ // the translation unit - which desyncs this file's declarations from FBTCPSocket.m's own
12
+ // #if TARGET_OS_WATCH branch.
13
+ #import <TargetConditionals.h>
14
+
15
+ #if TARGET_OS_WATCH
16
+ @import Foundation;
17
+ // A textual import, not `@import Network;` - older Xcode/watchOS SDK combinations (verified:
18
+ // Xcode 15.4/watchOS 10.5) fail to expose nw_listener_t/nw_connection_t and friends through the
19
+ // Network module map on watchOS, even though the underlying API has existed since watchOS 5.0.
20
+ #import <Network/Network.h>
21
+ #else
9
22
  #import "GCDAsyncSocket.h"
23
+ #endif
10
24
 
11
25
  NS_ASSUME_NONNULL_BEGIN
12
26
 
27
+ #if TARGET_OS_WATCH
28
+
29
+ // watchOS forbids BSD sockets, so this is backed by Network.framework instead of GCDAsyncSocket -
30
+ // hence a differently-shaped, push-style delegate protocol.
31
+ @protocol FBTCPSocketDelegate <NSObject>
32
+
33
+ /**
34
+ The callback which is fired on new TCP client connection
35
+
36
+ @param newClient The newly connected client
37
+ */
38
+ - (void)didClientConnect:(nw_connection_t)newClient;
39
+
40
+ /**
41
+ The callback which is fired when the TCP server receives data from a connected client
42
+
43
+ @param client The client, which sent the data
44
+ @param data The received data
45
+ */
46
+ - (void)client:(nw_connection_t)client didReceiveData:(NSData *)data;
47
+
48
+ /**
49
+ The callback which is fired when TCP client disconnects
50
+
51
+ @param client The actual disconnected client
52
+ */
53
+ - (void)didClientDisconnect:(nw_connection_t)client;
54
+
55
+ @end
56
+
57
+ #else
58
+
13
59
  @protocol FBTCPSocketDelegate
14
60
 
15
61
  /**
@@ -35,6 +81,8 @@ NS_ASSUME_NONNULL_BEGIN
35
81
 
36
82
  @end
37
83
 
84
+ #endif
85
+
38
86
 
39
87
  @interface FBTCPSocket : NSObject
40
88
 
@@ -64,6 +112,28 @@ NS_ASSUME_NONNULL_BEGIN
64
112
  Stops the socket if it is running
65
113
  */
66
114
  - (void)stop;
115
+
116
+ #if TARGET_OS_WATCH
117
+ /**
118
+ Writes data to the given connected client
119
+
120
+ @param data The data to send
121
+ @param client The destination client, as received via -didClientConnect: or -client:didReceiveData:
122
+ */
123
+ - (void)writeData:(NSData *)data toClient:(nw_connection_t)client;
124
+
125
+ /**
126
+ Like -writeData:toClient:, but invokes completion once the send actually goes out. Use this
127
+ before cancelling the connection - nw_connection_send is async, so cancelling right away can
128
+ drop the write before it's sent.
129
+
130
+ @param data The data to send
131
+ @param client The destination client
132
+ @param completion Called once the send attempt finishes
133
+ */
134
+ - (void)writeData:(NSData *)data toClient:(nw_connection_t)client completion:(nullable void (^)(void))completion;
135
+ #endif
136
+
67
137
  @end
68
138
 
69
139
  NS_ASSUME_NONNULL_END
@@ -8,6 +8,203 @@
8
8
 
9
9
  #import "FBTCPSocket.h"
10
10
 
11
+ #if TARGET_OS_WATCH
12
+
13
+ @interface FBTCPSocket()
14
+ @property (readonly, nonatomic) dispatch_queue_t socketQueue;
15
+ @property (nullable, nonatomic) nw_listener_t listener;
16
+ @property (readonly, nonatomic) NSMutableArray<nw_connection_t> *connectedClients;
17
+ @property (readonly, nonatomic) uint16_t port;
18
+ @end
19
+
20
+
21
+ @implementation FBTCPSocket
22
+
23
+ - (instancetype)initWithPort:(uint16_t)port
24
+ {
25
+ if ((self = [super init])) {
26
+ _socketQueue = dispatch_queue_create("socketQueue", NULL);
27
+ _connectedClients = [[NSMutableArray alloc] initWithCapacity:1];
28
+ _port = port;
29
+ _delegate = nil;
30
+ }
31
+ return self;
32
+ }
33
+
34
+ - (BOOL)startWithError:(NSError **)error
35
+ {
36
+ nw_parameters_t parameters = nw_parameters_create_secure_tcp(NW_PARAMETERS_DISABLE_PROTOCOL,
37
+ NW_PARAMETERS_DEFAULT_CONFIGURATION);
38
+ NSString *portString = [NSString stringWithFormat:@"%u", (unsigned int)self.port];
39
+ // portString is always valid UTF8; -UTF8String is just declared nullable in general.
40
+ const char * _Nonnull portCString = (const char * _Nonnull)portString.UTF8String;
41
+ nw_listener_t listener = nw_listener_create_with_port(portCString, parameters);
42
+ if (nil == listener) {
43
+ if (error) {
44
+ *error = [NSError errorWithDomain:@"FBTCPSocket"
45
+ code:1
46
+ userInfo:@{NSLocalizedDescriptionKey: @"Failed to create the TCP listener"}];
47
+ }
48
+ return NO;
49
+ }
50
+ self.listener = listener;
51
+
52
+ __weak typeof(self) weakSelf = self;
53
+ nw_listener_set_queue(listener, self.socketQueue);
54
+ nw_listener_set_new_connection_handler(listener, ^(nw_connection_t connection) {
55
+ [weakSelf acceptConnection:connection];
56
+ });
57
+
58
+ dispatch_semaphore_t startupSemaphore = dispatch_semaphore_create(0);
59
+ __block NSError *startupError = nil;
60
+ nw_listener_set_state_changed_handler(listener, ^(nw_listener_state_t state, nw_error_t nwError) {
61
+ // if/else, not switch: -Wswitch-enum, -Wswitch-default, and -Wcovered-switch-default can't
62
+ // all be satisfied by one switch statement at once.
63
+ if (nw_listener_state_ready == state) {
64
+ dispatch_semaphore_signal(startupSemaphore);
65
+ } else if (nw_listener_state_failed == state || nw_listener_state_cancelled == state) {
66
+ // NSLocalizedDescriptionKey must be a string, not the underlying NSError itself, or
67
+ // -[NSError localizedDescription] crashes trying to treat it as one.
68
+ NSError *underlyingError = nwError ? (NSError *)CFBridgingRelease(nw_error_copy_cf_error(nwError)) : nil;
69
+ NSMutableDictionary<NSString *, id> *userInfo = [NSMutableDictionary dictionary];
70
+ userInfo[NSLocalizedDescriptionKey] = underlyingError.localizedDescription ?: @"The TCP listener failed to start";
71
+ if (underlyingError) {
72
+ userInfo[NSUnderlyingErrorKey] = underlyingError;
73
+ }
74
+ startupError = [NSError errorWithDomain:@"FBTCPSocket" code:2 userInfo:userInfo];
75
+ dispatch_semaphore_signal(startupSemaphore);
76
+ }
77
+ });
78
+ nw_listener_start(listener);
79
+ BOOL didStartInTime = 0 == dispatch_semaphore_wait(startupSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)));
80
+ if (!didStartInTime) {
81
+ startupError = [NSError errorWithDomain:@"FBTCPSocket"
82
+ code:3
83
+ userInfo:@{NSLocalizedDescriptionKey: @"Timed out while starting the TCP listener"}];
84
+ }
85
+ if (nil != startupError) {
86
+ if (error) {
87
+ *error = startupError;
88
+ }
89
+ self.listener = nil;
90
+ return NO;
91
+ }
92
+ return YES;
93
+ }
94
+
95
+ - (void)acceptConnection:(nw_connection_t)connection
96
+ {
97
+ @synchronized (self.connectedClients) {
98
+ [self.connectedClients addObject:connection];
99
+ }
100
+
101
+ __weak typeof(self) weakSelf = self;
102
+ nw_connection_set_queue(connection, self.socketQueue);
103
+ nw_connection_set_state_changed_handler(connection, ^(nw_connection_state_t state, nw_error_t connectionError) {
104
+ // Same reasoning as the listener state handler above.
105
+ if (nw_connection_state_ready == state) {
106
+ __strong typeof(weakSelf) strongSelf = weakSelf;
107
+ if (nil == strongSelf) {
108
+ return;
109
+ }
110
+ id<FBTCPSocketDelegate> delegate = strongSelf.delegate;
111
+ if (nil != delegate) {
112
+ [delegate didClientConnect:connection];
113
+ }
114
+ [strongSelf scheduleReceiveForConnection:connection];
115
+ } else if (nw_connection_state_failed == state || nw_connection_state_cancelled == state) {
116
+ [weakSelf handleDisconnectForConnection:connection];
117
+ }
118
+ });
119
+ nw_connection_start(connection);
120
+ }
121
+
122
+ - (void)scheduleReceiveForConnection:(nw_connection_t)connection
123
+ {
124
+ __weak typeof(self) weakSelf = self;
125
+ nw_connection_receive(connection, 1, UINT32_MAX, ^(dispatch_data_t _Nullable content,
126
+ nw_content_context_t _Nullable context,
127
+ bool isComplete,
128
+ nw_error_t _Nullable receiveError) {
129
+ __strong typeof(weakSelf) strongSelf = weakSelf;
130
+ if (nil == strongSelf) {
131
+ return;
132
+ }
133
+ if (nil != content) {
134
+ dispatch_data_t nonnullContent = (dispatch_data_t _Nonnull)content;
135
+ __block NSData *data = nil;
136
+ dispatch_data_apply(nonnullContent, ^bool(dispatch_data_t _Nonnull region, size_t offset, const void * _Nonnull buffer, size_t size) {
137
+ NSMutableData *accumulated = [(data ?: [NSData data]) mutableCopy];
138
+ [accumulated appendBytes:buffer length:size];
139
+ data = accumulated.copy;
140
+ return true;
141
+ });
142
+ if (data.length > 0) {
143
+ id<FBTCPSocketDelegate> delegate = strongSelf.delegate;
144
+ if (nil != delegate) {
145
+ [delegate client:connection didReceiveData:data];
146
+ }
147
+ }
148
+ }
149
+ if (nil != receiveError || (isComplete && nil == content)) {
150
+ [strongSelf handleDisconnectForConnection:connection];
151
+ return;
152
+ }
153
+ [strongSelf scheduleReceiveForConnection:connection];
154
+ });
155
+ }
156
+
157
+ - (void)handleDisconnectForConnection:(nw_connection_t)connection
158
+ {
159
+ BOOL wasConnected;
160
+ @synchronized (self.connectedClients) {
161
+ wasConnected = [self.connectedClients containsObject:connection];
162
+ [self.connectedClients removeObject:connection];
163
+ }
164
+ if (wasConnected) {
165
+ id<FBTCPSocketDelegate> delegate = self.delegate;
166
+ if (nil != delegate) {
167
+ [delegate didClientDisconnect:connection];
168
+ }
169
+ }
170
+ }
171
+
172
+ - (void)writeData:(NSData *)data toClient:(nw_connection_t)client
173
+ {
174
+ [self writeData:data toClient:client completion:nil];
175
+ }
176
+
177
+ - (void)writeData:(NSData *)data toClient:(nw_connection_t)client completion:(nullable void (^)(void))completion
178
+ {
179
+ dispatch_data_t dispatchData = dispatch_data_create(data.bytes, data.length, self.socketQueue, DISPATCH_DATA_DESTRUCTOR_DEFAULT);
180
+ nw_connection_send(client, dispatchData, NW_CONNECTION_DEFAULT_STREAM_CONTEXT, false, ^(nw_error_t _Nullable sendError) {
181
+ if (completion) {
182
+ completion();
183
+ }
184
+ });
185
+ }
186
+
187
+ - (void)stop
188
+ {
189
+ @synchronized (self.connectedClients) {
190
+ NSArray<nw_connection_t> *clients = self.connectedClients.copy;
191
+ [self.connectedClients removeAllObjects];
192
+ for (nw_connection_t client in clients) {
193
+ nw_connection_cancel(client);
194
+ }
195
+ }
196
+
197
+ self.delegate = nil;
198
+ nw_listener_t listener = self.listener;
199
+ if (nil != listener) {
200
+ nw_listener_cancel((nw_listener_t _Nonnull)listener);
201
+ self.listener = nil;
202
+ }
203
+ }
204
+
205
+ @end
206
+
207
+ #else
11
208
 
12
209
  @interface FBTCPSocket()
13
210
  @property (readonly, nonatomic) dispatch_queue_t socketQueue;
@@ -95,3 +292,5 @@
95
292
  }
96
293
 
97
294
  @end
295
+
296
+ #endif