// 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; } } }