Skip to content

Commit 09b1dc2

Browse files
authored
Merge branch 'master' into sergeyd/android-utf8-conversion-fix
2 parents 8e40a21 + c465599 commit 09b1dc2

File tree

10 files changed

+226
-22
lines changed

10 files changed

+226
-22
lines changed

Libraries/BatchedBridge/BatchedBridge.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,21 @@
1212
'use strict';
1313

1414
const MessageQueue = require('MessageQueue');
15-
const BatchedBridge = new MessageQueue();
15+
16+
// MessageQueue can install a global handler to catch all exceptions where JS users can register their own behavior
17+
// This handler makes all exceptions to be handled inside MessageQueue rather than by the VM at its origin
18+
// This makes stacktraces to be placed at MessageQueue rather than at where they were launched
19+
// The parameter __fbUninstallRNGlobalErrorHandler is passed to MessageQueue to prevent the handler from being installed
20+
//
21+
// __fbUninstallRNGlobalErrorHandler is conditionally set by the Inspector while the VM is paused for intialization
22+
// If the Inspector isn't present it defaults to undefined and the global error handler is installed
23+
// The Inspector can still call MessageQueue#uninstallGlobalErrorHandler to uninstalled on attach
24+
25+
const BatchedBridge = new MessageQueue(
26+
// $FlowFixMe
27+
typeof __fbUninstallRNGlobalErrorHandler !== 'undefined' &&
28+
__fbUninstallRNGlobalErrorHandler === true, // eslint-disable-line no-undef
29+
);
1630

1731
// Wire up the batched bridge on the global object so that we can call into it.
1832
// Ideally, this would be the inverse relationship. I.e. the native environment

Libraries/BatchedBridge/MessageQueue.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,21 @@ class MessageQueue {
5959

6060
__spy: ?(data: SpyData) => void;
6161

62-
constructor() {
62+
__guard: (() => void) => void;
63+
64+
constructor(shouldUninstallGlobalErrorHandler: boolean = false) {
6365
this._lazyCallableModules = {};
6466
this._queue = [[], [], [], 0];
6567
this._successCallbacks = [];
6668
this._failureCallbacks = [];
6769
this._callID = 0;
6870
this._lastFlush = 0;
6971
this._eventLoopStartTime = new Date().getTime();
72+
if (shouldUninstallGlobalErrorHandler) {
73+
this.uninstallGlobalErrorHandler();
74+
} else {
75+
this.installGlobalErrorHandler();
76+
}
7077

7178
if (__DEV__) {
7279
this._debugInfo = {};
@@ -252,11 +259,26 @@ class MessageQueue {
252259
}
253260
}
254261

262+
uninstallGlobalErrorHandler() {
263+
this.__guard = this.__guardUnsafe;
264+
}
265+
266+
installGlobalErrorHandler() {
267+
this.__guard = this.__guardSafe;
268+
}
269+
255270
/**
256271
* Private methods
257272
*/
258273

259-
__guard(fn: () => void) {
274+
// Lets exceptions propagate to be handled by the VM at the origin
275+
__guardUnsafe(fn: () => void) {
276+
this._inCall++;
277+
fn();
278+
this._inCall--;
279+
}
280+
281+
__guardSafe(fn: () => void) {
260282
this._inCall++;
261283
try {
262284
fn();

Libraries/BatchedBridge/__tests__/MessageQueue-test.js

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* of patent rights can be found in the PATENTS file in the same directory.
88
*
99
* @emails oncall+react_native
10+
* @format
1011
*/
1112
'use strict';
1213

@@ -38,10 +39,12 @@ describe('MessageQueue', function() {
3839
queue = new MessageQueue();
3940
queue.registerCallableModule(
4041
'MessageQueueTestModule',
41-
MessageQueueTestModule
42+
MessageQueueTestModule,
4243
);
43-
queue.createDebugLookup(0, 'MessageQueueTestModule',
44-
['testHook1', 'testHook2']);
44+
queue.createDebugLookup(0, 'MessageQueueTestModule', [
45+
'testHook1',
46+
'testHook2',
47+
]);
4548
});
4649

4750
it('should enqueue native calls', () => {
@@ -65,7 +68,15 @@ describe('MessageQueue', function() {
6568

6669
it('should call the stored callback', () => {
6770
let done = false;
68-
queue.enqueueNativeCall(0, 1, [], () => {}, () => { done = true; });
71+
queue.enqueueNativeCall(
72+
0,
73+
1,
74+
[],
75+
() => {},
76+
() => {
77+
done = true;
78+
},
79+
);
6980
queue.__invokeCallback(1, []);
7081
expect(done).toEqual(true);
7182
});
@@ -83,32 +94,92 @@ describe('MessageQueue', function() {
8394
});
8495

8596
it('should throw when calling with unknown module', () => {
86-
const unknownModule = 'UnknownModule', unknownMethod = 'UnknownMethod';
97+
const unknownModule = 'UnknownModule',
98+
unknownMethod = 'UnknownMethod';
8799
expect(() => queue.__callFunction(unknownModule, unknownMethod)).toThrow(
88100
`Module ${unknownModule} is not a registered callable module (calling ${unknownMethod})`,
89101
);
90102
});
91103

92104
it('should return lazily registered module', () => {
93-
const dummyModule = {}, name = 'modulesName';
105+
const dummyModule = {},
106+
name = 'modulesName';
94107
queue.registerLazyCallableModule(name, () => dummyModule);
95108

96109
expect(queue.getCallableModule(name)).toEqual(dummyModule);
97110
});
98111

99112
it('should not initialize lazily registered module before it was used for the first time', () => {
100-
const dummyModule = {}, name = 'modulesName';
113+
const dummyModule = {},
114+
name = 'modulesName';
101115
const factory = jest.fn(() => dummyModule);
102116
queue.registerLazyCallableModule(name, factory);
103117
expect(factory).not.toHaveBeenCalled();
104118
});
105119

106120
it('should initialize lazily registered module only once', () => {
107-
const dummyModule = {}, name = 'modulesName';
121+
const dummyModule = {},
122+
name = 'modulesName';
108123
const factory = jest.fn(() => dummyModule);
109124
queue.registerLazyCallableModule(name, factory);
110125
queue.getCallableModule(name);
111126
queue.getCallableModule(name);
112127
expect(factory).toHaveBeenCalledTimes(1);
113128
});
129+
130+
it('should catch all exceptions if the global error handler is installed', () => {
131+
const errorMessage = 'intentional error';
132+
const errorModule = {
133+
explode: function() {
134+
throw new Error(errorMessage);
135+
},
136+
};
137+
const name = 'errorModuleName';
138+
const factory = jest.fn(() => errorModule);
139+
queue.__guardSafe = jest.fn(() => {});
140+
queue.__guardUnsafe = jest.fn(() => {});
141+
queue.installGlobalErrorHandler();
142+
queue.registerLazyCallableModule(name, factory);
143+
queue.callFunctionReturnFlushedQueue(name, 'explode', []);
144+
expect(queue.__guardUnsafe).toHaveBeenCalledTimes(0);
145+
expect(queue.__guardSafe).toHaveBeenCalledTimes(2);
146+
});
147+
148+
it('should propagate exceptions if the global error handler is uninstalled', () => {
149+
queue.uninstallGlobalErrorHandler();
150+
const errorMessage = 'intentional error';
151+
const errorModule = {
152+
explode: function() {
153+
throw new Error(errorMessage);
154+
},
155+
};
156+
const name = 'errorModuleName';
157+
const factory = jest.fn(() => errorModule);
158+
queue.__guardUnsafe = jest.fn(() => {});
159+
queue.__guardSafe = jest.fn(() => {});
160+
queue.registerLazyCallableModule(name, factory);
161+
queue.uninstallGlobalErrorHandler();
162+
queue.callFunctionReturnFlushedQueue(name, 'explode');
163+
expect(queue.__guardUnsafe).toHaveBeenCalledTimes(2);
164+
expect(queue.__guardSafe).toHaveBeenCalledTimes(0);
165+
});
166+
167+
it('should catch all exceptions if the global error handler is re-installed', () => {
168+
const errorMessage = 'intentional error';
169+
const errorModule = {
170+
explode: function() {
171+
throw new Error(errorMessage);
172+
},
173+
};
174+
const name = 'errorModuleName';
175+
const factory = jest.fn(() => errorModule);
176+
queue.__guardUnsafe = jest.fn(() => {});
177+
queue.__guardSafe = jest.fn(() => {});
178+
queue.registerLazyCallableModule(name, factory);
179+
queue.uninstallGlobalErrorHandler();
180+
queue.installGlobalErrorHandler();
181+
queue.callFunctionReturnFlushedQueue(name, 'explode');
182+
expect(queue.__guardUnsafe).toHaveBeenCalledTimes(0);
183+
expect(queue.__guardSafe).toHaveBeenCalledTimes(2);
184+
});
114185
});

Libraries/Image/RCTImageLoader.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ typedef dispatch_block_t RCTImageLoaderCancellationBlock;
3838

3939
@end
4040

41+
/**
42+
* If available, RCTImageRedirectProtocol is invoked before loading an asset.
43+
* Implementation should return either a new URL or nil when redirection is
44+
* not needed.
45+
*/
46+
47+
@protocol RCTImageRedirectProtocol
48+
49+
- (NSURL *)redirectAssetsURL:(NSURL *)URL;
50+
51+
@end
52+
4153
@interface UIImage (React)
4254

4355
@property (nonatomic, copy) CAKeyframeAnimation *reactKeyframeAnimation;
@@ -71,6 +83,9 @@ typedef dispatch_block_t RCTImageLoaderCancellationBlock;
7183
*/
7284
@property (nonatomic, assign) NSUInteger maxConcurrentDecodingBytes;
7385

86+
- (instancetype)init;
87+
- (instancetype)initWithRedirectDelegate:(id<RCTImageRedirectProtocol>)redirectDelegate NS_DESIGNATED_INITIALIZER;
88+
7489
/**
7590
* Loads the specified image at the highest available resolution.
7691
* Can be called from any thread, will call back on an unspecified thread.

Libraries/Image/RCTImageLoader.m

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,26 @@ @implementation RCTImageLoader
4848
NSMutableArray *_pendingDecodes;
4949
NSInteger _scheduledDecodes;
5050
NSUInteger _activeBytes;
51+
__weak id<RCTImageRedirectProtocol> _redirectDelegate;
5152
}
5253

5354
@synthesize bridge = _bridge;
5455

5556
RCT_EXPORT_MODULE()
5657

58+
- (instancetype)init
59+
{
60+
return [self initWithRedirectDelegate:nil];
61+
}
62+
63+
- (instancetype)initWithRedirectDelegate:(id<RCTImageRedirectProtocol>)redirectDelegate
64+
{
65+
if (self = [super init]) {
66+
_redirectDelegate = redirectDelegate;
67+
}
68+
return self;
69+
}
70+
5771
- (void)setUp
5872
{
5973
// Set defaults
@@ -316,6 +330,9 @@ - (RCTImageLoaderCancellationBlock)_loadImageOrDataWithURLRequest:(NSURLRequest
316330
if (request.URL.fileURL && request.URL.pathExtension.length == 0) {
317331
mutableRequest.URL = [request.URL URLByAppendingPathExtension:@"png"];
318332
}
333+
if (_redirectDelegate != nil) {
334+
mutableRequest.URL = [_redirectDelegate redirectAssetsURL:mutableRequest.URL];
335+
}
319336
request = mutableRequest;
320337
}
321338

Libraries/RCTTest/RCTTestRunner.m

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,15 @@ - (void)runTest:(SEL)test module:(NSString *)moduleName
117117
__weak RCTBridge *batchedBridge;
118118

119119
@autoreleasepool {
120-
__block NSString *error = nil;
120+
__block NSMutableArray<NSString *> *errors = nil;
121121
RCTLogFunction defaultLogFunction = RCTGetLogFunction();
122122
RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {
123123
defaultLogFunction(level, source, fileName, lineNumber, message);
124124
if (level >= RCTLogLevelError) {
125-
error = message;
125+
if (errors == nil) {
126+
errors = [NSMutableArray new];
127+
}
128+
[errors addObject:message];
126129
}
127130
});
128131

@@ -155,7 +158,7 @@ - (void)runTest:(SEL)test module:(NSString *)moduleName
155158
}
156159

157160
NSDate *date = [NSDate dateWithTimeIntervalSinceNow:kTestTimeoutSeconds];
158-
while (date.timeIntervalSinceNow > 0 && testModule.status == RCTTestStatusPending && error == nil) {
161+
while (date.timeIntervalSinceNow > 0 && testModule.status == RCTTestStatusPending && errors == nil) {
159162
[[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
160163
[[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
161164
}
@@ -173,9 +176,9 @@ - (void)runTest:(SEL)test module:(NSString *)moduleName
173176
#endif
174177

175178
if (expectErrorBlock) {
176-
RCTAssert(expectErrorBlock(error), @"Expected an error but nothing matched.");
179+
RCTAssert(expectErrorBlock(errors[0]), @"Expected an error but the first one was missing or did not match.");
177180
} else {
178-
RCTAssert(error == nil, @"RedBox error: %@", error);
181+
RCTAssert(errors == nil, @"RedBox errors: %@", errors);
179182
RCTAssert(testModule.status != RCTTestStatusPending, @"Test didn't finish within %0.f seconds", kTestTimeoutSeconds);
180183
RCTAssert(testModule.status == RCTTestStatusPassed, @"Test failed");
181184
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @providesModule PlatformOS
10+
* @flow
11+
*/
12+
13+
'use strict';
14+
15+
export type PlatformSelectSpec<A, I> = {|
16+
android: A,
17+
ios: I,
18+
|};
19+
20+
const PlatformOS = {
21+
OS: 'android',
22+
select: <A, I> (spec: PlatformSelectSpec<A, I>): A | I => spec.android,
23+
};
24+
25+
module.exports = PlatformOS;

Libraries/Utilities/PlatformOS.ios.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @providesModule PlatformOS
10+
* @flow
11+
*/
12+
13+
'use strict';
14+
15+
export type PlatformSelectSpec<A, I> = {|
16+
android: A,
17+
ios: I,
18+
|};
19+
20+
const PlatformOS = {
21+
OS: 'ios',
22+
select: <A, I> (spec: PlatformSelectSpec<A, I>): A | I => spec.ios,
23+
};
24+
25+
module.exports = PlatformOS;

0 commit comments

Comments
 (0)