Unverified Commit e3ee5c6b authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Add better handling of JSON-RPC exception (#36082)

parent d919e694
...@@ -844,7 +844,7 @@ abstract class ResidentRunner { ...@@ -844,7 +844,7 @@ abstract class ResidentRunner {
} }
class OperationResult { class OperationResult {
OperationResult(this.code, this.message); OperationResult(this.code, this.message, { this.fatal = false });
/// The result of the operation; a non-zero code indicates a failure. /// The result of the operation; a non-zero code indicates a failure.
final int code; final int code;
...@@ -852,6 +852,9 @@ class OperationResult { ...@@ -852,6 +852,9 @@ class OperationResult {
/// A user facing message about the results of the operation. /// A user facing message about the results of the operation.
final String message; final String message;
/// Whether this error should cause the runner to exit.
final bool fatal;
bool get isOk => code == 0; bool get isOk => code == 0;
static final OperationResult ok = OperationResult(0, ''); static final OperationResult ok = OperationResult(0, '');
...@@ -906,8 +909,14 @@ class TerminalHandler { ...@@ -906,8 +909,14 @@ class TerminalHandler {
void registerSignalHandlers() { void registerSignalHandlers() {
assert(residentRunner.stayResident); assert(residentRunner.stayResident);
io.ProcessSignal.SIGINT.watch().listen(_cleanUpAndExit); io.ProcessSignal.SIGINT.watch().listen((io.ProcessSignal signal) {
io.ProcessSignal.SIGTERM.watch().listen(_cleanUpAndExit); _cleanUp(signal);
io.exit(0);
});
io.ProcessSignal.SIGTERM.watch().listen((io.ProcessSignal signal) {
_cleanUp(signal);
io.exit(0);
});
if (!residentRunner.supportsServiceProtocol || !residentRunner.supportsRestart) if (!residentRunner.supportsServiceProtocol || !residentRunner.supportsRestart)
return; return;
io.ProcessSignal.SIGUSR1.watch().listen(_handleSignal); io.ProcessSignal.SIGUSR1.watch().listen(_handleSignal);
...@@ -990,6 +999,9 @@ class TerminalHandler { ...@@ -990,6 +999,9 @@ class TerminalHandler {
return false; return false;
} }
final OperationResult result = await residentRunner.restart(fullRestart: false); final OperationResult result = await residentRunner.restart(fullRestart: false);
if (result.fatal) {
throwToolExit(result.message);
}
if (!result.isOk) { if (!result.isOk) {
printStatus('Try again after fixing the above error(s).', emphasis: true); printStatus('Try again after fixing the above error(s).', emphasis: true);
} }
...@@ -1000,6 +1012,9 @@ class TerminalHandler { ...@@ -1000,6 +1012,9 @@ class TerminalHandler {
return false; return false;
} }
final OperationResult result = await residentRunner.restart(fullRestart: true); final OperationResult result = await residentRunner.restart(fullRestart: true);
if (result.fatal) {
throwToolExit(result.message);
}
if (!result.isOk) { if (!result.isOk) {
printStatus('Try again after fixing the above error(s).', emphasis: true); printStatus('Try again after fixing the above error(s).', emphasis: true);
} }
...@@ -1051,7 +1066,8 @@ class TerminalHandler { ...@@ -1051,7 +1066,8 @@ class TerminalHandler {
await _commonTerminalInputHandler(command); await _commonTerminalInputHandler(command);
} catch (error, st) { } catch (error, st) {
printError('$error\n$st'); printError('$error\n$st');
await _cleanUpAndExit(null); await _cleanUp(null);
rethrow;
} finally { } finally {
_processingUserRequest = false; _processingUserRequest = false;
} }
...@@ -1073,11 +1089,10 @@ class TerminalHandler { ...@@ -1073,11 +1089,10 @@ class TerminalHandler {
} }
} }
Future<void> _cleanUpAndExit(io.ProcessSignal signal) async { Future<void> _cleanUp(io.ProcessSignal signal) async {
terminal.singleCharMode = false; terminal.singleCharMode = false;
await subscription.cancel(); await subscription?.cancel();
await residentRunner.cleanupAfterSignal(); await residentRunner.cleanupAfterSignal();
io.exit(0);
} }
} }
......
...@@ -516,6 +516,9 @@ class HotRunner extends ResidentRunner { ...@@ -516,6 +516,9 @@ class HotRunner extends ResidentRunner {
final OperationResult result = await _restartFromSources(reason: reason, benchmarkMode: benchmarkMode,); final OperationResult result = await _restartFromSources(reason: reason, benchmarkMode: benchmarkMode,);
if (!result.isOk) if (!result.isOk)
return result; return result;
} on rpc.RpcException {
await _measureJsonRpcException(flutterDevices, fullRestart);
return OperationResult(1, 'hot restart failed to complete', fatal: true);
} finally { } finally {
status.cancel(); status.cancel();
} }
...@@ -545,6 +548,9 @@ class HotRunner extends ResidentRunner { ...@@ -545,6 +548,9 @@ class HotRunner extends ResidentRunner {
showTime = false; showTime = false;
}, },
); );
} on rpc.RpcException {
await _measureJsonRpcException(flutterDevices, fullRestart);
return OperationResult(1, 'hot reload failed to complete', fatal: true);
} finally { } finally {
status.cancel(); status.cancel();
} }
...@@ -946,3 +952,32 @@ class ProjectFileInvalidator { ...@@ -946,3 +952,32 @@ class ProjectFileInvalidator {
return invalidatedFiles; return invalidatedFiles;
} }
} }
// This is an error case we would like to know more about.
Future<void> _measureJsonRpcException(List<FlutterDevice> flutterDevices, bool fullRestart) async {
String targetPlatform;
String deviceSdk;
bool emulator;
if (flutterDevices.length == 1) {
final Device device = flutterDevices.first.device;
targetPlatform = getNameForTargetPlatform(await device.targetPlatform);
deviceSdk = await device.sdkNameAndVersion;
emulator = await device.isLocalEmulator;
} else if (flutterDevices.length > 1) {
targetPlatform = 'multiple';
deviceSdk = 'multiple';
emulator = false;
} else {
targetPlatform = 'unknown';
deviceSdk = 'unknown';
emulator = false;
}
flutterUsage.sendEvent('hot', 'exception',
parameters: <String, String>{
reloadExceptionTargetPlatform: targetPlatform,
reloadExceptionSdkName: deviceSdk,
reloadExceptionEmulator: emulator.toString(),
reloadExceptionFullRestart: fullRestart.toString(),
},
);
}
...@@ -49,7 +49,12 @@ const String kCommandBuildBundleTargetPlatform = 'cd24'; ...@@ -49,7 +49,12 @@ const String kCommandBuildBundleTargetPlatform = 'cd24';
const String kCommandBuildBundleIsModule = 'cd25'; const String kCommandBuildBundleIsModule = 'cd25';
const String kCommandResult = 'cd26'; const String kCommandResult = 'cd26';
// Next ID: cd27
const String reloadExceptionTargetPlatform = 'cd27';
const String reloadExceptionSdkName = 'cd28';
const String reloadExceptionEmulator = 'cd29';
const String reloadExceptionFullRestart = 'cd30';
// Next ID: cd31
Usage get flutterUsage => Usage.instance; Usage get flutterUsage => Usage.instance;
......
...@@ -4,12 +4,15 @@ ...@@ -4,12 +4,15 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/run_hot.dart'; import 'package:flutter_tools/src/run_hot.dart';
import 'package:flutter_tools/src/usage.dart';
import 'package:flutter_tools/src/vmservice.dart'; import 'package:flutter_tools/src/vmservice.dart';
import 'package:json_rpc_2/json_rpc_2.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import '../src/common.dart'; import '../src/common.dart';
...@@ -19,30 +22,34 @@ void main() { ...@@ -19,30 +22,34 @@ void main() {
group('ResidentRunner', () { group('ResidentRunner', () {
final Uri testUri = Uri.parse('foo://bar'); final Uri testUri = Uri.parse('foo://bar');
Testbed testbed; Testbed testbed;
MockDevice mockDevice; MockFlutterDevice mockFlutterDevice;
MockVMService mockVMService; MockVMService mockVMService;
MockDevFS mockDevFS; MockDevFS mockDevFS;
MockFlutterView mockFlutterView;
ResidentRunner residentRunner; ResidentRunner residentRunner;
MockDevice mockDevice;
setUp(() { setUp(() {
testbed = Testbed(setup: () { testbed = Testbed(setup: () {
residentRunner = HotRunner( residentRunner = HotRunner(
<FlutterDevice>[ <FlutterDevice>[
mockDevice, mockFlutterDevice,
], ],
stayResident: false, stayResident: false,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
); );
}); });
mockFlutterDevice = MockFlutterDevice();
mockDevice = MockDevice(); mockDevice = MockDevice();
mockVMService = MockVMService(); mockVMService = MockVMService();
mockDevFS = MockDevFS(); mockDevFS = MockDevFS();
mockFlutterView = MockFlutterView();
// DevFS Mocks // DevFS Mocks
when(mockDevFS.lastCompiled).thenReturn(DateTime(2000)); when(mockDevFS.lastCompiled).thenReturn(DateTime(2000));
when(mockDevFS.sources).thenReturn(<Uri>[]); when(mockDevFS.sources).thenReturn(<Uri>[]);
when(mockDevFS.destroy()).thenAnswer((Invocation invocation) async { }); when(mockDevFS.destroy()).thenAnswer((Invocation invocation) async { });
// FlutterDevice Mocks. // FlutterDevice Mocks.
when(mockDevice.updateDevFS( when(mockFlutterDevice.updateDevFS(
// Intentionally provide empty list to match above mock. // Intentionally provide empty list to match above mock.
invalidatedFiles: <Uri>[], invalidatedFiles: <Uri>[],
mainPath: anyNamed('mainPath'), mainPath: anyNamed('mainPath'),
...@@ -61,27 +68,29 @@ void main() { ...@@ -61,27 +68,29 @@ void main() {
invalidatedSourcesCount: 0, invalidatedSourcesCount: 0,
); );
}); });
when(mockDevice.devFS).thenReturn(mockDevFS); when(mockFlutterDevice.devFS).thenReturn(mockDevFS);
when(mockDevice.views).thenReturn(<FlutterView>[ when(mockFlutterDevice.views).thenReturn(<FlutterView>[
MockFlutterView(), mockFlutterView
]); ]);
when(mockDevice.stopEchoingDeviceLog()).thenAnswer((Invocation invocation) async { }); when(mockFlutterDevice.device).thenReturn(mockDevice);
when(mockDevice.observatoryUris).thenReturn(<Uri>[ when(mockFlutterView.uiIsolate).thenReturn(MockIsolate());
when(mockFlutterDevice.stopEchoingDeviceLog()).thenAnswer((Invocation invocation) async { });
when(mockFlutterDevice.observatoryUris).thenReturn(<Uri>[
testUri, testUri,
]); ]);
when(mockDevice.connect( when(mockFlutterDevice.connect(
reloadSources: anyNamed('reloadSources'), reloadSources: anyNamed('reloadSources'),
restart: anyNamed('restart'), restart: anyNamed('restart'),
compileExpression: anyNamed('compileExpression') compileExpression: anyNamed('compileExpression')
)).thenAnswer((Invocation invocation) async { }); )).thenAnswer((Invocation invocation) async { });
when(mockDevice.setupDevFS(any, any, packagesFilePath: anyNamed('packagesFilePath'))) when(mockFlutterDevice.setupDevFS(any, any, packagesFilePath: anyNamed('packagesFilePath')))
.thenAnswer((Invocation invocation) async { .thenAnswer((Invocation invocation) async {
return testUri; return testUri;
}); });
when(mockDevice.vmServices).thenReturn(<VMService>[ when(mockFlutterDevice.vmServices).thenReturn(<VMService>[
mockVMService, mockVMService,
]); ]);
when(mockDevice.refreshViews()).thenAnswer((Invocation invocation) async { }); when(mockFlutterDevice.refreshViews()).thenAnswer((Invocation invocation) async { });
// VMService mocks. // VMService mocks.
when(mockVMService.wsAddress).thenReturn(testUri); when(mockVMService.wsAddress).thenReturn(testUri);
when(mockVMService.done).thenAnswer((Invocation invocation) { when(mockVMService.done).thenAnswer((Invocation invocation) {
...@@ -101,16 +110,106 @@ void main() { ...@@ -101,16 +110,106 @@ void main() {
expect(await result, 0); expect(await result, 0);
verify(mockDevice.initLogReader()).called(1); verify(mockFlutterDevice.initLogReader()).called(1);
expect(onConnectionInfo.isCompleted, true); expect(onConnectionInfo.isCompleted, true);
expect((await connectionInfo).baseUri, 'foo://bar'); expect((await connectionInfo).baseUri, 'foo://bar');
expect(onAppStart.isCompleted, true); expect(onAppStart.isCompleted, true);
})); }));
test('Can handle an RPC exception from hot reload', () => testbed.run(() async {
when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async {
return 'Example';
});
when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async {
return TargetPlatform.android_arm;
});
when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) async {
return false;
});
final Completer<DebugConnectionInfo> onConnectionInfo = Completer<DebugConnectionInfo>.sync();
final Completer<void> onAppStart = Completer<void>.sync();
unawaited(residentRunner.attach(
appStartedCompleter: onAppStart,
connectionInfoCompleter: onConnectionInfo,
));
await onAppStart.future;
when(mockFlutterDevice.updateDevFS(
mainPath: anyNamed('mainPath'),
target: anyNamed('target'),
bundle: anyNamed('bundle'),
firstBuildTime: anyNamed('firstBuildTime'),
bundleFirstUpload: anyNamed('bundleFirstUpload'),
bundleDirty: anyNamed('bundleDirty'),
fullRestart: anyNamed('fullRestart'),
projectRootPath: anyNamed('projectRootPath'),
pathToReload: anyNamed('pathToReload'),
invalidatedFiles: anyNamed('invalidatedFiles'),
)).thenThrow(RpcException(666, 'something bad happened'));
final OperationResult result = await residentRunner.restart(fullRestart: false);
expect(result.fatal, true);
expect(result.code, 1);
verify(flutterUsage.sendEvent('hot', 'exception', parameters: <String, String>{
reloadExceptionTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm),
reloadExceptionSdkName: 'Example',
reloadExceptionEmulator: 'false',
reloadExceptionFullRestart: 'false',
})).called(1);
}, overrides: <Type, Generator>{
Usage: () => MockUsage(),
}));
test('Can handle an RPC exception from hot restart', () => testbed.run(() async {
when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async {
return 'Example';
});
when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async {
return TargetPlatform.android_arm;
});
when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) async {
return false;
});
when(mockDevice.supportsHotRestart).thenReturn(true);
final Completer<DebugConnectionInfo> onConnectionInfo = Completer<DebugConnectionInfo>.sync();
final Completer<void> onAppStart = Completer<void>.sync();
unawaited(residentRunner.attach(
appStartedCompleter: onAppStart,
connectionInfoCompleter: onConnectionInfo,
));
await onAppStart.future;
when(mockFlutterDevice.updateDevFS(
mainPath: anyNamed('mainPath'),
target: anyNamed('target'),
bundle: anyNamed('bundle'),
firstBuildTime: anyNamed('firstBuildTime'),
bundleFirstUpload: anyNamed('bundleFirstUpload'),
bundleDirty: anyNamed('bundleDirty'),
fullRestart: anyNamed('fullRestart'),
projectRootPath: anyNamed('projectRootPath'),
pathToReload: anyNamed('pathToReload'),
invalidatedFiles: anyNamed('invalidatedFiles'),
)).thenThrow(RpcException(666, 'something bad happened'));
final OperationResult result = await residentRunner.restart(fullRestart: true);
expect(result.fatal, true);
expect(result.code, 1);
verify(flutterUsage.sendEvent('hot', 'exception', parameters: <String, String>{
reloadExceptionTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm),
reloadExceptionSdkName: 'Example',
reloadExceptionEmulator: 'false',
reloadExceptionFullRestart: 'true',
})).called(1);
}, overrides: <Type, Generator>{
Usage: () => MockUsage(),
}));
}); });
} }
class MockDevice extends Mock implements FlutterDevice {} class MockFlutterDevice extends Mock implements FlutterDevice {}
class MockFlutterView extends Mock implements FlutterView {} class MockFlutterView extends Mock implements FlutterView {}
class MockVMService extends Mock implements VMService {} class MockVMService extends Mock implements VMService {}
class MockDevFS extends Mock implements DevFS {} class MockDevFS extends Mock implements DevFS {}
class MockIsolate extends Mock implements Isolate {}
class MockDevice extends Mock implements Device {}
class MockUsage extends Mock implements Usage {}
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
...@@ -246,6 +247,16 @@ void main() { ...@@ -246,6 +247,16 @@ void main() {
expect(bufferLogger.statusText, contains('Try again after fixing the above error(s).')); expect(bufferLogger.statusText, contains('Try again after fixing the above error(s).'));
}); });
testUsingContext('r - hotReload supported and fails fatally', () async {
when(mockResidentRunner.canHotReload).thenReturn(true);
when(mockResidentRunner.hotMode).thenReturn(true);
when(mockResidentRunner.restart(fullRestart: false))
.thenAnswer((Invocation invocation) async {
return OperationResult(1, 'fail', fatal: true);
});
expect(terminalHandler.processTerminalInput('r'), throwsA(isInstanceOf<ToolExit>()));
});
testUsingContext('r - hotReload unsupported', () async { testUsingContext('r - hotReload unsupported', () async {
when(mockResidentRunner.canHotReload).thenReturn(false); when(mockResidentRunner.canHotReload).thenReturn(false);
await terminalHandler.processTerminalInput('r'); await terminalHandler.processTerminalInput('r');
...@@ -281,6 +292,16 @@ void main() { ...@@ -281,6 +292,16 @@ void main() {
expect(bufferLogger.statusText, contains('Try again after fixing the above error(s).')); expect(bufferLogger.statusText, contains('Try again after fixing the above error(s).'));
}); });
testUsingContext('R - hotRestart supported and fails fatally', () async {
when(mockResidentRunner.canHotRestart).thenReturn(true);
when(mockResidentRunner.hotMode).thenReturn(true);
when(mockResidentRunner.restart(fullRestart: true))
.thenAnswer((Invocation invocation) async {
return OperationResult(1, 'fail', fatal: true);
});
expect(() => terminalHandler.processTerminalInput('R'), throwsA(isInstanceOf<ToolExit>()));
});
testUsingContext('R - hot restart unsupported', () async { testUsingContext('R - hot restart unsupported', () async {
when(mockResidentRunner.canHotRestart).thenReturn(false); when(mockResidentRunner.canHotRestart).thenReturn(false);
await terminalHandler.processTerminalInput('R'); await terminalHandler.processTerminalInput('R');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment