blob: a494143d03f2fc28212d95f9ee6d4f90c47adc64 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:dwds/dwds.dart';
import 'package:meta/meta.dart';
import 'package:vm_service/vm_service.dart' as vmservice;
import '../application_package.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/terminal.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../convert.dart';
import '../device.dart';
import '../globals.dart';
import '../project.dart';
import '../resident_runner.dart';
import '../web/web_runner.dart';
import 'web_fs.dart';
/// Injectable factory to create a [ResidentWebRunner].
class DwdsWebRunnerFactory extends WebRunnerFactory {
@override
ResidentRunner createWebRunner(
Device device, {
String target,
@required FlutterProject flutterProject,
@required bool ipv6,
@required DebuggingOptions debuggingOptions
}) {
return ResidentWebRunner(
device,
target: target,
flutterProject: flutterProject,
debuggingOptions: debuggingOptions,
ipv6: ipv6,
);
}
}
// TODO(jonahwilliams): remove this constant when the error message is removed.
// The web engine is currently spamming this message on certain pages. Filter it out
// until we remove it entirely. See flutter/flutter##37625.
const String _kBadError = 'WARNING: 3D transformation matrix was passed to BitmapCanvas.';
/// A hot-runner which handles browser specific delegation.
class ResidentWebRunner extends ResidentRunner {
ResidentWebRunner(this.device, {
String target,
@required this.flutterProject,
@required bool ipv6,
@required DebuggingOptions debuggingOptions,
}) : super(
<FlutterDevice>[],
target: target,
debuggingOptions: debuggingOptions,
ipv6: ipv6,
stayResident: true,
);
final Device device;
final FlutterProject flutterProject;
// Only the debug builds of the web support the service protocol.
@override
bool get supportsServiceProtocol => isRunningDebug;
WebFs _webFs;
DebugConnection _debugConnection;
StreamSubscription<vmservice.Event> _stdOutSub;
vmservice.VmService get _vmService => _debugConnection.vmService;
@override
bool get canHotRestart {
return true;
}
@override
Future<Map<String, dynamic>> invokeFlutterExtensionRpcRawOnFirstIsolate(
String method, {
Map<String, dynamic> params,
}) async {
final vmservice.Response response = await _vmService.callServiceExtension(method, args: params);
return response.toJson();
}
@override
Future<void> cleanupAfterSignal() async {
await _cleanup();
}
@override
Future<void> cleanupAtFinish() async {
await _cleanup();
}
Future<void> _cleanup() async {
await _debugConnection?.close();
await _stdOutSub?.cancel();
await _webFs?.stop();
}
@override
void printHelp({bool details = true}) {
if (details) {
return printHelpDetails();
}
const String fire = '🔥';
const String rawMessage =
' To hot restart (and rebuild state), press "R".';
final String message = terminal.color(
fire + terminal.bolden(rawMessage),
TerminalColor.red,
);
const String warning = '👻 ';
printStatus(warning * 20);
printStatus('Warning: Flutter\'s support for building web applications is highly experimental.');
printStatus('For more information see https://github.com/flutter/flutter/issues/34082.');
printStatus(warning * 20);
printStatus('');
printStatus(message);
const String quitMessage = 'To quit, press "q".';
printStatus('For a more detailed help message, press "h". $quitMessage');
}
@override
Future<int> run({
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
String route,
}) async {
final ApplicationPackage package = await ApplicationPackageFactory.instance.getPackageForPlatform(
TargetPlatform.web_javascript,
applicationBinary: null,
);
if (package == null) {
printError('No application found for TargetPlatform.web_javascript.');
printError('To add web support to a project, run `flutter create --web .`.');
return 1;
}
if (!fs.isFileSync(mainPath)) {
String message = 'Tried to run $mainPath, but that file does not exist.';
if (target == null) {
message +=
'\nConsider using the -t option to specify the Dart file to start.';
}
printError(message);
return 1;
}
Status buildStatus;
try {
buildStatus = logger.startProgress('Building application for the web...', timeout: null);
_webFs = await webFsFactory(
target: target,
flutterProject: flutterProject,
buildInfo: debuggingOptions.buildInfo,
);
if (supportsServiceProtocol) {
_debugConnection = await _webFs.runAndDebug();
unawaited(_debugConnection.onDone.whenComplete(exit));
}
} catch (err, stackTrace) {
printError(err.toString());
printError(stackTrace.toString());
throwToolExit('Failed to build application for the web.');
} finally {
buildStatus.stop();
}
appStartedCompleter?.complete();
return attach(
connectionInfoCompleter: connectionInfoCompleter,
appStartedCompleter: appStartedCompleter,
);
}
@override
Future<int> attach({
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
}) async {
// Cleanup old subscriptions. These will throw if there isn't anything
// listening, which is fine because that is what we want to ensure.
try {
await _debugConnection?.vmService?.streamCancel('Stdout');
} on vmservice.RPCError {
// Ignore this specific error.
}
try {
await _debugConnection?.vmService?.streamListen('Stdout');
} on vmservice.RPCError {
// Ignore this specific error.
}
Uri websocketUri;
if (supportsServiceProtocol) {
_stdOutSub = _debugConnection.vmService.onStdoutEvent.listen((vmservice.Event log) {
final String message = utf8.decode(base64.decode(log.bytes)).trim();
// TODO(jonahwilliams): remove this error once it is gone from the engine #37625.
if (!message.contains(_kBadError)) {
printStatus(message);
}
});
websocketUri = Uri.parse(_debugConnection.uri);
}
if (websocketUri != null) {
printStatus('Debug service listening on $websocketUri.');
}
connectionInfoCompleter?.complete(
DebugConnectionInfo(wsUri: websocketUri)
);
final int result = await waitForAppToFinish();
await cleanupAtFinish();
return result;
}
@override
Future<OperationResult> restart({
bool fullRestart = false,
bool pauseAfterRestart = false,
String reason,
bool benchmarkMode = false,
}) async {
if (!fullRestart) {
return OperationResult(1, 'hot reload not supported on the web.');
}
final Stopwatch timer = Stopwatch()..start();
final Status status = logger.startProgress(
'Performing hot restart...',
timeout: supportsServiceProtocol
? timeoutConfiguration.fastOperation
: timeoutConfiguration.slowOperation,
progressId: 'hot.restart',
);
final bool success = await _webFs.recompile();
if (!success) {
status.stop();
return OperationResult(1, 'Failed to recompile application.');
}
if (supportsServiceProtocol) {
try {
final vmservice.Response reloadResponse = await _vmService.callServiceExtension('hotRestart');
printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.');
return reloadResponse.type == 'Success'
? OperationResult.ok
: OperationResult(1, reloadResponse.toString());
} on vmservice.RPCError {
await _webFs.hardRefresh();
return OperationResult(1, 'Page requires full reload');
} finally {
status.stop();
}
}
// If we're not in hot mode, the only way to restart is to reload the tab.
await _webFs.hardRefresh();
status.stop();
return OperationResult.ok;
}
@override
Future<void> debugDumpApp() async {
try {
await _vmService.callServiceExtension(
'ext.flutter.debugDumpApp',
);
} on vmservice.RPCError {
return;
}
}
@override
Future<void> debugDumpRenderTree() async {
try {
await _vmService.callServiceExtension(
'ext.flutter.debugDumpRenderTree',
);
} on vmservice.RPCError {
return;
}
}
@override
Future<void> debugDumpLayerTree() async {
try {
await _vmService.callServiceExtension(
'ext.flutter.debugDumpLayerTree',
);
} on vmservice.RPCError {
return;
}
}
@override
Future<void> debugDumpSemanticsTreeInTraversalOrder() async {
try {
await _vmService.callServiceExtension(
'ext.flutter.debugDumpSemanticsTreeInTraversalOrder');
} on vmservice.RPCError {
return;
}
}
@override
Future<void> debugDumpSemanticsTreeInInverseHitTestOrder() async {
try {
await _vmService.callServiceExtension(
'ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder');
} on vmservice.RPCError {
return;
}
}
@override
Future<void> debugToggleDebugPaintSizeEnabled() async {
try {
final vmservice.Response response = await _vmService.callServiceExtension(
'ext.flutter.debugPaint',
);
await _vmService.callServiceExtension(
'ext.flutter.debugPaint',
args: <dynamic, dynamic>{'enabled': !(response.json['enabled'] == 'true')},
);
} on vmservice.RPCError {
return;
}
}
@override
Future<void> debugToggleDebugCheckElevationsEnabled() async {
try {
final vmservice.Response response = await _vmService.callServiceExtension(
'ext.flutter.debugCheckElevationsEnabled',
);
await _vmService.callServiceExtension(
'ext.flutter.debugCheckElevationsEnabled',
args: <dynamic, dynamic>{'enabled': !(response.json['enabled'] == 'true')},
);
} on vmservice.RPCError {
return;
}
}
@override
Future<void> debugTogglePerformanceOverlayOverride() async {
try {
final vmservice.Response response = await _vmService.callServiceExtension(
'ext.flutter.showPerformanceOverlay'
);
await _vmService.callServiceExtension(
'ext.flutter.showPerformanceOverlay',
args: <dynamic, dynamic>{'enabled': !(response.json['enabled'] == 'true')},
);
} on vmservice.RPCError {
return;
}
}
@override
Future<void> debugToggleWidgetInspector() async {
try {
final vmservice.Response response = await _vmService.callServiceExtension(
'ext.flutter.debugToggleWidgetInspector'
);
await _vmService.callServiceExtension(
'ext.flutter.debugToggleWidgetInspector',
args: <dynamic, dynamic>{'enabled': !(response.json['enabled'] == 'true')},
);
} on vmservice.RPCError {
return;
}
}
@override
Future<void> debugToggleProfileWidgetBuilds() async {
try {
final vmservice.Response response = await _vmService.callServiceExtension(
'ext.flutter.profileWidgetBuilds'
);
await _vmService.callServiceExtension(
'ext.flutter.profileWidgetBuilds',
args: <dynamic, dynamic>{'enabled': !(response.json['enabled'] == 'true')},
);
} on vmservice.RPCError {
return;
}
}
}