ensure _futurize does not leak uncaught errors into the zone (flutter/engine#32070)

This commit is contained in:
Jonah Williams 2022-03-17 11:05:06 -07:00 committed by GitHub
parent ae5d2a1ba7
commit ab2ea2006c
3 changed files with 117 additions and 3 deletions

View File

@ -747,9 +747,98 @@ void hooksTests() {
expectEquals(frameNumber, 2);
});
test('_futureize handles callbacker sync error', () async {
String? callbacker(void Function(Object? arg) cb) {
return 'failure';
}
Object? error;
try {
await _futurize(callbacker);
} catch (err) {
error = err;
}
expectNotEquals(error, null);
});
test('_futureize does not leak sync uncaught exceptions into the zone', () async {
String? callbacker(void Function(Object? arg) cb) {
cb(null); // indicates failure
}
Object? error;
try {
await _futurize(callbacker);
} catch (err) {
error = err;
}
expectNotEquals(error, null);
});
test('_futureize does not leak async uncaught exceptions into the zone', () async {
String? callbacker(void Function(Object? arg) cb) {
Timer.run(() {
cb(null); // indicates failure
});
}
Object? error;
try {
await _futurize(callbacker);
} catch (err) {
error = err;
}
expectNotEquals(error, null);
});
test('_futureize successfully returns a value sync', () async {
String? callbacker(void Function(Object? arg) cb) {
cb(true);
}
final Object? result = await _futurize(callbacker);
expectEquals(result, true);
});
test('_futureize successfully returns a value async', () async {
String? callbacker(void Function(Object? arg) cb) {
Timer.run(() {
cb(true);
});
}
final Object? result = await _futurize(callbacker);
expectEquals(result, true);
});
_finish();
}
typedef _Callback<T> = void Function(T result);
typedef _Callbacker<T> = String? Function(_Callback<T?> callback);
// This is an exact copy of the function defined in painting.dart. If you change either
// then you must change both.
Future<T> _futurize<T>(_Callbacker<T> callbacker) {
final Completer<T> completer = Completer<T>.sync();
// If the callback synchronously throws an error, then synchronously
// rethrow that error instead of adding it to the completer. This
// prevents the Zone from receiving an uncaught exception.
bool sync = true;
final String? error = callbacker((T? t) {
if (t == null) {
if (sync) {
throw Exception('operation failed');
} else {
completer.completeError(Exception('operation failed'));
}
} else {
completer.complete(t);
}
});
sync = false;
if (error != null)
throw Exception(error);
return completer.future;
}
void _callHook(
String name, [
int argCount = 0,

View File

@ -5599,15 +5599,27 @@ typedef _Callbacker<T> = String? Function(_Callback<T?> callback);
/// return _futurize(_doSomethingAndCallback);
/// }
/// ```
// Note: this function is not directly tested so that it remains private, instead an exact
// copy of it has been inlined into the test at lib/ui/fixtures/ui_test.dart. if you change
// this function, then you must update the test.
Future<T> _futurize<T>(_Callbacker<T> callbacker) {
final Completer<T> completer = Completer<T>.sync();
// If the callback synchronously throws an error, then synchronously
// rethrow that error instead of adding it to the completer. This
// prevents the Zone from receiving an uncaught exception.
bool sync = true;
final String? error = callbacker((T? t) {
if (t == null) {
completer.completeError(Exception('operation failed'));
if (sync) {
throw Exception('operation failed');
} else {
completer.completeError(Exception('operation failed'));
}
} else {
completer.complete(t);
}
});
sync = false;
if (error != null)
throw Exception(error);
return completer.future;

View File

@ -139,15 +139,28 @@ final PlatformViewRegistry platformViewRegistry = PlatformViewRegistry();
// NNBD migration.
typedef _Callback<T> = void Function(T result);
typedef _Callbacker<T> = String? Function(_Callback<T> callback);
// Note: this function is not directly tested so that it remains private, instead an exact
// copy of it has been inlined into the test at lib/ui/fixtures/ui_test.dart. if you change
// this function, then you must update the test.
Future<T> _futurize<T>(_Callbacker<T> callbacker) {
final Completer<T> completer = Completer<T>.sync();
final String? error = callbacker((T t) {
// If the callback synchronously throws an error, then synchronously
// rethrow that error instead of adding it to the completer. This
// prevents the Zone from receiving an uncaught exception.
bool sync = true;
final String? error = callbacker((T? t) {
if (t == null) {
completer.completeError(Exception('operation failed'));
if (sync) {
throw Exception('operation failed');
} else {
completer.completeError(Exception('operation failed'));
}
} else {
completer.complete(t);
}
});
sync = false;
if (error != null) {
throw Exception(error);
}