Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions WebDriverAgentLib/Commands/FBScreenCaptureCommands.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

#import "FBBroadcastManager.h"
#import "FBConfiguration.h"
#import "FBLogger.h"
#import "FBRouteRequest.h"
#import "FBVideoStreamManager.h"

Expand All @@ -30,10 +31,12 @@ + (NSArray *)routes
// otherwise be swallowed by 'GET /mobilerun/screencapture/:id'.
[[FBRoute POST:@"/mobilerun/screencapture/broadcast/start"] respondWithTarget:self action:@selector(handleStartBroadcast:)],
[[FBRoute POST:@"/mobilerun/screencapture/broadcast/stop"] respondWithTarget:self action:@selector(handleStopBroadcast:)],
// Not marked onControlQueue: decorating a session-required route reads FBSession's static
// active-session state, which the automation queue writes without synchronization.
[[FBRoute GET:@"/mobilerun/screencapture/broadcast"] respondWithTarget:self action:@selector(handleGetBroadcastStatus:)],
[[FBRoute POST:@"/mobilerun/screencapture/broadcast/start"].withoutSession respondWithTarget:self action:@selector(handleStartBroadcast:)],
[[FBRoute POST:@"/mobilerun/screencapture/broadcast/stop"].withoutSession respondWithTarget:self action:@selector(handleStopBroadcast:)],
[[FBRoute GET:@"/mobilerun/screencapture/broadcast"].withoutSession respondWithTarget:self action:@selector(handleGetBroadcastStatus:)],
[[[FBRoute GET:@"/mobilerun/screencapture/broadcast"].withoutSession onControlQueue] respondWithTarget:self action:@selector(handleGetBroadcastStatus:)],

[[FBRoute POST:@"/mobilerun/screencapture/start"] respondWithTarget:self action:@selector(handleStartScreenCapture:)],
[[FBRoute POST:@"/mobilerun/screencapture/stop"] respondWithTarget:self action:@selector(handleStopAllScreenCapture:)],
Expand All @@ -43,11 +46,11 @@ + (NSArray *)routes
[[FBRoute POST:@"/mobilerun/screencapture/:id/keyframe"] respondWithTarget:self action:@selector(handleRequestKeyFrame:)],

[[FBRoute POST:@"/mobilerun/screencapture/start"].withoutSession respondWithTarget:self action:@selector(handleStartScreenCapture:)],
[[FBRoute POST:@"/mobilerun/screencapture/stop"].withoutSession respondWithTarget:self action:@selector(handleStopAllScreenCapture:)],
[[FBRoute GET:@"/mobilerun/screencapture"].withoutSession respondWithTarget:self action:@selector(handleListScreenCapture:)],
[[FBRoute GET:@"/mobilerun/screencapture/:id"].withoutSession respondWithTarget:self action:@selector(handleGetScreenCapture:)],
[[FBRoute POST:@"/mobilerun/screencapture/:id/stop"].withoutSession respondWithTarget:self action:@selector(handleStopScreenCapture:)],
[[FBRoute POST:@"/mobilerun/screencapture/:id/keyframe"].withoutSession respondWithTarget:self action:@selector(handleRequestKeyFrame:)],
[[[FBRoute POST:@"/mobilerun/screencapture/stop"].withoutSession onControlQueue] respondWithTarget:self action:@selector(handleStopAllScreenCapture:)],
[[[FBRoute GET:@"/mobilerun/screencapture"].withoutSession onControlQueue] respondWithTarget:self action:@selector(handleListScreenCapture:)],
[[[FBRoute GET:@"/mobilerun/screencapture/:id"].withoutSession onControlQueue] respondWithTarget:self action:@selector(handleGetScreenCapture:)],
[[[FBRoute POST:@"/mobilerun/screencapture/:id/stop"].withoutSession onControlQueue] respondWithTarget:self action:@selector(handleStopScreenCapture:)],
[[[FBRoute POST:@"/mobilerun/screencapture/:id/keyframe"].withoutSession onControlQueue] respondWithTarget:self action:@selector(handleRequestKeyFrame:)],
];
}

Expand Down Expand Up @@ -126,6 +129,21 @@ + (NSArray *)routes
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Both 'width' and 'height' must be provided as positive integers" traceback:nil]);
}

NSUInteger pixelBudget = 0;
NSUInteger deviceDefaultBudget = [FBScreenCaptureConfiguration fb_defaultPixelBudgetForMachineModel:[FBScreenCaptureConfiguration fb_machineModel]];
if (![FBScreenCaptureConfiguration fb_pixelBudget:&pixelBudget
fromArgument:request.arguments[@"maxPixels"]
deviceDefault:deviceDefaultBudget]) {
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"'maxPixels' must be 0 (uncapped) or an integer of at least 4" traceback:nil]);
}
CGSize cappedSize = [FBScreenCaptureConfiguration fb_sizeForWidth:(NSUInteger)width height:(NSUInteger)height pixelBudget:pixelBudget];
if ((NSInteger)cappedSize.width < width || (NSInteger)cappedSize.height < height) {
[FBLogger logFmt:@"Capping the requested capture size %ldx%ld to %ldx%ld (pixel budget %lu)",
(long)width, (long)height, (long)cappedSize.width, (long)cappedSize.height, (unsigned long)pixelBudget];
width = (NSInteger)cappedSize.width;
height = (NSInteger)cappedSize.height;
}

FBScreenCaptureConfiguration *configuration = [[FBScreenCaptureConfiguration alloc] init];
configuration.codec = codec;
configuration.framing = framing;
Expand Down
2 changes: 1 addition & 1 deletion WebDriverAgentLib/Commands/FBSessionCommands.m
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ + (NSArray *)routes
[[FBRoute GET:@"/wda/apps/list"] respondWithTarget:self action:@selector(handleGetActiveAppsList:)],
[[FBRoute GET:@""] respondWithTarget:self action:@selector(handleGetActiveSession:)],
[[FBRoute DELETE:@""] respondWithTarget:self action:@selector(handleDeleteSession:)],
[[FBRoute GET:@"/status"].withoutSession respondWithTarget:self action:@selector(handleGetStatus:)],
[[[FBRoute GET:@"/status"].withoutSession onControlQueue] respondWithTarget:self action:@selector(handleGetStatus:)],

// Health check might modify simulator state so it should only be called in-between testing sessions
[[FBRoute GET:@"/wda/healthcheck"].withoutSession respondWithTarget:self action:@selector(handleGetHealthCheck:)],
Expand Down
8 changes: 4 additions & 4 deletions WebDriverAgentLib/Commands/FBUnknownCommands.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ + (NSArray *)routes
{
return
@[
[[FBRoute GET:@"/*"].withoutSession respondWithTarget:self action:@selector(unhandledHandler:)],
[[FBRoute POST:@"/*"].withoutSession respondWithTarget:self action:@selector(unhandledHandler:)],
[[FBRoute PUT:@"/*"].withoutSession respondWithTarget:self action:@selector(unhandledHandler:)],
[[FBRoute DELETE:@"/*"].withoutSession respondWithTarget:self action:@selector(unhandledHandler:)]
[[[FBRoute GET:@"/*"].withoutSession onControlQueue] respondWithTarget:self action:@selector(unhandledHandler:)],
[[[FBRoute POST:@"/*"].withoutSession onControlQueue] respondWithTarget:self action:@selector(unhandledHandler:)],
[[[FBRoute PUT:@"/*"].withoutSession onControlQueue] respondWithTarget:self action:@selector(unhandledHandler:)],
[[[FBRoute DELETE:@"/*"].withoutSession onControlQueue] respondWithTarget:self action:@selector(unhandledHandler:)]
];
}

Expand Down
12 changes: 12 additions & 0 deletions WebDriverAgentLib/Routing/FBRoute.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ typedef __nonnull id<FBResponsePayload> (^FBRouteSyncHandler)(FBRouteRequest *re
/*! Route's path */
@property (nonatomic, copy, readonly) NSString *path;

/*! YES when the route is served directly on the HTTP connection's queue instead of the
automation (main) queue */
@property (nonatomic, assign, readonly) BOOL usesControlQueue;

/**
Convenience constructor for GET route with given pathPattern
*/
Expand Down Expand Up @@ -67,6 +71,14 @@ typedef __nonnull id<FBResponsePayload> (^FBRouteSyncHandler)(FBRouteRequest *re
*/
- (instancetype)withoutSession;

/**
Chain-able modifier that marks the route to be served on the HTTP connection's own queue,
bypassing the automation (main) queue. Only routes whose handlers never call XCUI or
testmanagerd APIs and only touch thread-safe state may opt in — such routes stay responsive
even while an automation request is blocked.
*/
- (instancetype)onControlQueue;

/**
Dispatches response for request
*/
Expand Down
9 changes: 9 additions & 0 deletions WebDriverAgentLib/Routing/FBRoute.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

@interface FBRoute ()
@property (nonatomic, assign, readwrite) BOOL requiresSession;
@property (nonatomic, assign, readwrite) BOOL usesControlQueue;
@property (nonatomic, copy, readwrite) NSString *verb;
@property (nonatomic, copy, readwrite) NSString *path;

Expand Down Expand Up @@ -126,10 +127,17 @@ - (instancetype)withoutSession
return self;
}

- (instancetype)onControlQueue
{
self.usesControlQueue = YES;
return self;
}

- (instancetype)respondWithBlock:(FBRouteSyncHandler)handler
{
FBRoute_Sync *route = [FBRoute_Sync withVerb:self.verb path:self.path requiresSession:self.requiresSession];
route.handler = handler;
route.usesControlQueue = self.usesControlQueue;
return route;
}

Expand All @@ -138,6 +146,7 @@ - (instancetype)respondWithTarget:(id)target action:(SEL)action
FBRoute_TargetAction *route = [FBRoute_TargetAction withVerb:self.verb path:self.path requiresSession:self.requiresSession];
route.target = target;
route.action = action;
route.usesControlQueue = self.usesControlQueue;
return route;
}

Expand Down
35 changes: 27 additions & 8 deletions WebDriverAgentLib/Routing/FBSession.m
Original file line number Diff line number Diff line change
Expand Up @@ -89,30 +89,43 @@ - (void)didDetectAlert:(FBAlert *)alert

@implementation FBSession

// Control routes (e.g. /status) are served on their own connection queue and read this
// static concurrently with main-queue writes in markSessionActive:/kill. All reads and
// writes of _activeSession must go through the @synchronized (FBSession.class) accessors
// below.
static FBSession *_activeSession = nil;

+ (instancetype)activeSession
{
return _activeSession;
@synchronized (FBSession.class) {
return _activeSession;
}
}

+ (void)markSessionActive:(FBSession *)session
{
if (_activeSession) {
[_activeSession kill];
FBSession *previousSession;
@synchronized (FBSession.class) {
previousSession = _activeSession;
}
if (previousSession) {
[previousSession kill];
}
@synchronized (FBSession.class) {
_activeSession = session;
}
_activeSession = session;
}

+ (instancetype)sessionWithIdentifier:(NSString *)identifier
{
if (!identifier) {
return nil;
}
if (![identifier isEqualToString:_activeSession.identifier]) {
FBSession *activeSession = self.activeSession;
if (![identifier isEqualToString:activeSession.identifier]) {
return nil;
}
return _activeSession;
return activeSession;
}

+ (instancetype)initWithApplication:(XCUIApplication *)application
Expand Down Expand Up @@ -169,7 +182,11 @@ - (BOOL)disableAlertsMonitor

- (void)kill
{
if (nil == _activeSession) {
BOOL wasActive;
@synchronized (FBSession.class) {
wasActive = (nil != _activeSession);
}
if (!wasActive) {
return;
}

Expand All @@ -195,7 +212,9 @@ - (void)kill
}
}

_activeSession = nil;
@synchronized (FBSession.class) {
_activeSession = nil;
}
}

- (XCUIApplication *)activeApplication
Expand Down
59 changes: 53 additions & 6 deletions WebDriverAgentLib/Routing/FBWebServer.m
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#import "FBUnknownCommands.h"
#import "FBConfiguration.h"
#import "FBLogger.h"
#import "FBXCodeCompatibility.h"

#import "XCUIDevice+FBHelpers.h"

Expand Down Expand Up @@ -65,6 +66,9 @@ @interface FBWebServer ()
@property (nonatomic, nullable, strong) FBMjpegServer *mjpegServer;
#endif
@property (atomic, assign) BOOL keepAlive;
// Serializes automation requests onto a single funnel so at most one is ever in flight on
// the main queue. See registerRouteHandlers: for why this is necessary.
@property (nonatomic, strong) dispatch_queue_t automationQueue;
@end

@implementation FBWebServer
Expand Down Expand Up @@ -105,6 +109,17 @@ - (void)startServing
#endif

self.keepAlive = YES;
// /status is served off the main queue (it uses onControlQueue), but FBSDKVersion() and
// FBTestmanagerdVersion() cache their result behind a dispatch_once. Burn both once-tokens
// here, on the main thread, warmed only after the server has bound: FBTestmanagerdVersion()'s
// legacy branch waits (with a bounded timeout) on the daemon, and a degraded daemon must not
// be able to prevent the server from binding. An early request that races the warm-up just
// blocks on the dispatch_once for at most the bounded handshake. Warmed only after
// initialization is complete and keepAlive is set, so a shutdown that arrives while the
// bounded legacy handshake spins the run loop simply clears keepAlive via stopServing and the
// serving loop below never starts.
FBSDKVersion();
FBTestmanagerdVersion();
NSRunLoop *runLoop = [NSRunLoop mainRunLoop];
while (self.keepAlive) {
@try {
Expand Down Expand Up @@ -132,14 +147,18 @@ - (BOOL)startHTTPServer
#else
self.server = [[RoutingHTTPServer alloc] init];
#endif
#if TARGET_OS_WATCH
[self.server setRouteQueue:dispatch_get_main_queue()];
#endif
[self.server setDefaultHeader:@"Server" value:@"WebDriverAgent/1.0"];
[self.server setDefaultHeader:@"Access-Control-Allow-Origin" value:@"*"];
[self.server setDefaultHeader:@"Access-Control-Allow-Headers" value:@"Content-Type, X-Requested-With"];
#if !TARGET_OS_WATCH
[self.server setConnectionClass:[FBHTTPConnection self]];
#endif

self.automationQueue = dispatch_queue_create("com.facebook.WebDriverAgent.automation-funnel", DISPATCH_QUEUE_SERIAL);

[self registerRouteHandlers:[self.class collectCommandHandlerClasses]];
[self registerServerKeyRouteHandlers];

Expand Down Expand Up @@ -293,17 +312,41 @@ - (void)registerRouteHandlers:(NSArray *)commandHandlerClasses

[FBLogger verboseLog:routeParams.description];

@try {
[route mountRequest:routeParams intoResponse:response];
}
@catch (NSException *exception) {
[strongSelf handleException:exception forResponse:response];
#if TARGET_OS_WATCH
[strongSelf mountRoute:route request:routeParams intoResponse:response];
#else
if (route.usesControlQueue) {
// Served on this connection's own queue so it stays responsive while the automation
// queue is busy or blocked. Only routes that never touch XCUI state opt in.
[strongSelf mountRoute:route request:routeParams intoResponse:response];
} else {
// Serialize automation requests: while one is on the main queue (possibly spinning the
// run loop), the next waits here instead of being enqueued to main, where a nested run
// loop drain would otherwise execute it reentrantly inside the first handler.
dispatch_sync(strongSelf.automationQueue, ^{
dispatch_sync(dispatch_get_main_queue(), ^{
@autoreleasepool {
[strongSelf mountRoute:route request:routeParams intoResponse:response];
}
});
});
}
#endif
}];
}
}
}

- (void)mountRoute:(FBRoute *)route request:(FBRouteRequest *)routeParams intoResponse:(RouteResponse *)response
{
@try {
[route mountRequest:routeParams intoResponse:response];
}
@catch (NSException *exception) {
[self handleException:exception forResponse:response];
}
}

- (void)handleException:(NSException *)exception forResponse:(RouteResponse *)response
{
[self.exceptionHandler handleException:exception forResponse:response];
Expand Down Expand Up @@ -332,7 +375,11 @@ - (void)registerServerKeyRouteHandlers
return;
}
[response respondWithString:@"Shutting down"];
[strongSelf.delegate webServerDidRequestShutdown:strongSelf];
// The delegate tears down automation state; run it on the main queue without blocking
// this connection's queue.
dispatch_async(dispatch_get_main_queue(), ^{
[strongSelf.delegate webServerDidRequestShutdown:strongSelf];
});
}];

[self registerRouteHandlers:@[FBUnknownCommands.class]];
Expand Down
4 changes: 3 additions & 1 deletion WebDriverAgentLib/Utilities/FBBroadcastManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ static uint64_t FBBroadcastNowMs(void)

@interface FBBroadcastManager () <FBBroadcastControlServerDelegate>

@property (nonatomic, nullable) FBBroadcastControlServer *controlServer;
// Read from connection queues (broadcast status route, sessionless capture-stop notifications)
// while the main thread assigns/clears it - must stay atomic.
@property (atomic, nullable) FBBroadcastControlServer *controlServer;
@property (atomic, nullable, copy) NSDictionary *helloInfo;
@property (atomic, nullable, copy) NSDictionary *lastHeartbeat;
@property (atomic, nullable) NSDate *connectedAt;
Expand Down
8 changes: 8 additions & 0 deletions WebDriverAgentLib/Utilities/FBConfiguration.h
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ typedef NS_ENUM(NSInteger, FBConfigurationKeyboardPreference) {
*/
@property (atomic, readonly) UInt64 httpRequestBodySizeLimit;

/**
Extra time in seconds granted on top of a synthesized event's own scheduled duration before an
unacknowledged synthesis is failed with an error instead of blocking the caller forever.
Override with the EVENT_SYNTHESIS_TIMEOUT_MARGIN environment variable (a positive number of
seconds). Defaults to 15.
*/
- (NSTimeInterval)eventSynthesisTimeoutMargin;

/**
The default port number where the raw H.264/H.265 screen capture broadcaster is supposed to run.
The default value is 9200. It can be overridden via the SCREEN_CAPTURE_SERVER_PORT environment
Expand Down
13 changes: 13 additions & 0 deletions WebDriverAgentLib/Utilities/FBConfiguration.m
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
static NSUInteger const DefaultAudioCaptureServerPort = 9400;
static NSUInteger const DefaultPortRange = 100;
static UInt64 const DefaultHttpRequestBodySizeLimit = 1024ull * 1024ull * 1024ull;
static const NSTimeInterval DefaultEventSynthesisTimeoutMargin = 15.0;

static char const *const controllerPrefBundlePath = "/System/Library/PrivateFrameworks/TextInput.framework/TextInput";
static NSString *const controllerClassName = @"TIPreferencesController";
Expand Down Expand Up @@ -218,6 +219,18 @@ - (UInt64)httpRequestBodySizeLimit
return DefaultHttpRequestBodySizeLimit;
}

- (NSTimeInterval)eventSynthesisTimeoutMargin
{
const char *rawMargin = getenv("EVENT_SYNTHESIS_TIMEOUT_MARGIN");
if (rawMargin != NULL) {
double parsedMargin = atof(rawMargin);
if (parsedMargin > 0) {
return parsedMargin;
}
}
return DefaultEventSynthesisTimeoutMargin;
}

- (BOOL)verboseLoggingEnabled
{
return [NSProcessInfo.processInfo.environment[@"VERBOSE_LOGGING"] boolValue];
Expand Down
Loading
Loading