1// Copyright 2019 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:async'; 6 7import 'package:dwds/dwds.dart'; 8import 'package:meta/meta.dart'; 9import 'package:vm_service/vm_service.dart' as vmservice; 10 11import '../application_package.dart'; 12import '../base/common.dart'; 13import '../base/file_system.dart'; 14import '../base/logger.dart'; 15import '../base/terminal.dart'; 16import '../base/utils.dart'; 17import '../build_info.dart'; 18import '../convert.dart'; 19import '../device.dart'; 20import '../globals.dart'; 21import '../project.dart'; 22import '../resident_runner.dart'; 23import '../web/web_runner.dart'; 24import 'web_fs.dart'; 25 26/// Injectable factory to create a [ResidentWebRunner]. 27class DwdsWebRunnerFactory extends WebRunnerFactory { 28 @override 29 ResidentRunner createWebRunner( 30 Device device, { 31 String target, 32 @required FlutterProject flutterProject, 33 @required bool ipv6, 34 @required DebuggingOptions debuggingOptions 35 }) { 36 return ResidentWebRunner( 37 device, 38 target: target, 39 flutterProject: flutterProject, 40 debuggingOptions: debuggingOptions, 41 ipv6: ipv6, 42 ); 43 } 44} 45 46// TODO(jonahwilliams): remove this constant when the error message is removed. 47// The web engine is currently spamming this message on certain pages. Filter it out 48// until we remove it entirely. See flutter/flutter##37625. 49const String _kBadError = 'WARNING: 3D transformation matrix was passed to BitmapCanvas.'; 50 51/// A hot-runner which handles browser specific delegation. 52class ResidentWebRunner extends ResidentRunner { 53 ResidentWebRunner(this.device, { 54 String target, 55 @required this.flutterProject, 56 @required bool ipv6, 57 @required DebuggingOptions debuggingOptions, 58 }) : super( 59 <FlutterDevice>[], 60 target: target, 61 debuggingOptions: debuggingOptions, 62 ipv6: ipv6, 63 usesTerminalUi: true, 64 stayResident: true, 65 ); 66 67 final Device device; 68 final FlutterProject flutterProject; 69 70 WebFs _webFs; 71 DebugConnection _debugConnection; 72 StreamSubscription<vmservice.Event> _stdOutSub; 73 74 vmservice.VmService get _vmService => _debugConnection.vmService; 75 76 @override 77 bool get canHotRestart { 78 return true; 79 } 80 81 @override 82 Future<Map<String, dynamic>> invokeFlutterExtensionRpcRawOnFirstIsolate( 83 String method, { 84 Map<String, dynamic> params, 85 }) async { 86 final vmservice.Response response = await _vmService.callServiceExtension(method, args: params); 87 return response.toJson(); 88 } 89 90 @override 91 Future<void> cleanupAfterSignal() async { 92 await _cleanup(); 93 } 94 95 @override 96 Future<void> cleanupAtFinish() async { 97 await _cleanup(); 98 } 99 100 Future<void> _cleanup() async { 101 await _debugConnection?.close(); 102 await _stdOutSub?.cancel(); 103 await _webFs?.stop(); 104 } 105 106 @override 107 void printHelp({bool details = true}) { 108 if (details) { 109 return printHelpDetails(); 110 } 111 const String fire = ''; 112 const String rawMessage = 113 ' To hot restart (and rebuild state), press "R".'; 114 final String message = terminal.color( 115 fire + terminal.bolden(rawMessage), 116 TerminalColor.red, 117 ); 118 const String warning = ' '; 119 printStatus(warning * 20); 120 printStatus('Warning: Flutter\'s support for building web applications is highly experimental.'); 121 printStatus('For more information see https://github.com/flutter/flutter/issues/34082.'); 122 printStatus(warning * 20); 123 printStatus(''); 124 printStatus(message); 125 const String quitMessage = 'To quit, press "q".'; 126 printStatus('For a more detailed help message, press "h". $quitMessage'); 127 } 128 129 @override 130 Future<int> run({ 131 Completer<DebugConnectionInfo> connectionInfoCompleter, 132 Completer<void> appStartedCompleter, 133 String route, 134 }) async { 135 final ApplicationPackage package = await ApplicationPackageFactory.instance.getPackageForPlatform( 136 TargetPlatform.web_javascript, 137 applicationBinary: null, 138 ); 139 if (package == null) { 140 printError('No application found for TargetPlatform.web_javascript.'); 141 printError('To add web support to a project, run `flutter create --web .`.'); 142 return 1; 143 } 144 if (!fs.isFileSync(mainPath)) { 145 String message = 'Tried to run $mainPath, but that file does not exist.'; 146 if (target == null) { 147 message += 148 '\nConsider using the -t option to specify the Dart file to start.'; 149 } 150 printError(message); 151 return 1; 152 } 153 Status buildStatus; 154 try { 155 buildStatus = logger.startProgress('Building application for the web...', timeout: null); 156 _webFs = await webFsFactory( 157 target: target, 158 flutterProject: flutterProject, 159 buildInfo: debuggingOptions.buildInfo, 160 ); 161 if (supportsServiceProtocol) { 162 _debugConnection = await _webFs.runAndDebug(); 163 } 164 } catch (err, stackTrace) { 165 printError(err.toString()); 166 printError(stackTrace.toString()); 167 throwToolExit('Failed to build application for the web.'); 168 } finally { 169 buildStatus.stop(); 170 } 171 appStartedCompleter?.complete(); 172 return attach( 173 connectionInfoCompleter: connectionInfoCompleter, 174 appStartedCompleter: appStartedCompleter, 175 ); 176 } 177 178 @override 179 Future<int> attach({ 180 Completer<DebugConnectionInfo> connectionInfoCompleter, 181 Completer<void> appStartedCompleter, 182 }) async { 183 // Cleanup old subscriptions. These will throw if there isn't anything 184 // listening, which is fine because that is what we want to ensure. 185 try { 186 await _debugConnection?.vmService?.streamCancel('Stdout'); 187 } on vmservice.RPCError { 188 // Ignore this specific error. 189 } 190 try { 191 await _debugConnection?.vmService?.streamListen('Stdout'); 192 } on vmservice.RPCError { 193 // Ignore this specific error. 194 } 195 Uri websocketUri; 196 if (supportsServiceProtocol) { 197 _stdOutSub = _debugConnection.vmService.onStdoutEvent.listen((vmservice.Event log) { 198 final String message = utf8.decode(base64.decode(log.bytes)).trim(); 199 // TODO(jonahwilliams): remove this error once it is gone from the engine #37625. 200 if (!message.contains(_kBadError)) { 201 printStatus(message); 202 } 203 }); 204 websocketUri = Uri.parse(_debugConnection.uri); 205 } 206 connectionInfoCompleter?.complete( 207 DebugConnectionInfo(wsUri: websocketUri) 208 ); 209 final int result = await waitForAppToFinish(); 210 await cleanupAtFinish(); 211 return result; 212 } 213 214 @override 215 Future<OperationResult> restart({ 216 bool fullRestart = false, 217 bool pauseAfterRestart = false, 218 String reason, 219 bool benchmarkMode = false, 220 }) async { 221 if (!fullRestart) { 222 return OperationResult(1, 'hot reload not supported on the web.'); 223 } 224 final Stopwatch timer = Stopwatch()..start(); 225 final Status status = logger.startProgress( 226 'Performing hot restart...', 227 timeout: supportsServiceProtocol 228 ? timeoutConfiguration.fastOperation 229 : timeoutConfiguration.slowOperation, 230 progressId: 'hot.restart', 231 ); 232 final bool success = await _webFs.recompile(); 233 if (!success) { 234 status.stop(); 235 return OperationResult(1, 'Failed to recompile application.'); 236 } 237 if (supportsServiceProtocol) { 238 final vmservice.Response reloadResponse = await _vmService.callServiceExtension('hotRestart'); 239 status.stop(); 240 printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.'); 241 return reloadResponse.type == 'Success' 242 ? OperationResult.ok 243 : OperationResult(1, reloadResponse.toString()); 244 } 245 // If we're not in hot mode, the only way to restart is to reload the tab. 246 await _webFs.hardRefresh(); 247 status.stop(); 248 return OperationResult.ok; 249 } 250 251 @override 252 Future<void> debugDumpApp() async { 253 try { 254 await _vmService.callServiceExtension( 255 'ext.flutter.debugDumpApp', 256 ); 257 } on vmservice.RPCError { 258 return; 259 } 260 } 261 262 @override 263 Future<void> debugDumpRenderTree() async { 264 try { 265 await _vmService.callServiceExtension( 266 'ext.flutter.debugDumpRenderTree', 267 ); 268 } on vmservice.RPCError { 269 return; 270 } 271 } 272 273 @override 274 Future<void> debugDumpLayerTree() async { 275 try { 276 await _vmService.callServiceExtension( 277 'ext.flutter.debugDumpLayerTree', 278 ); 279 } on vmservice.RPCError { 280 return; 281 } 282 } 283 284 @override 285 Future<void> debugDumpSemanticsTreeInTraversalOrder() async { 286 try { 287 await _vmService.callServiceExtension( 288 'ext.flutter.debugDumpSemanticsTreeInTraversalOrder'); 289 } on vmservice.RPCError { 290 return; 291 } 292 } 293 294 @override 295 Future<void> debugDumpSemanticsTreeInInverseHitTestOrder() async { 296 try { 297 await _vmService.callServiceExtension( 298 'ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder'); 299 } on vmservice.RPCError { 300 return; 301 } 302 } 303 304 305 @override 306 Future<void> debugToggleDebugPaintSizeEnabled() async { 307 try { 308 final vmservice.Response response = await _vmService.callServiceExtension( 309 'ext.flutter.debugPaint', 310 ); 311 await _vmService.callServiceExtension( 312 'ext.flutter.debugPaint', 313 args: <dynamic, dynamic>{'enabled': !(response.json['enabled'] == 'true')}, 314 ); 315 } on vmservice.RPCError { 316 return; 317 } 318 } 319 320 @override 321 Future<void> debugToggleDebugCheckElevationsEnabled() async { 322 try { 323 final vmservice.Response response = await _vmService.callServiceExtension( 324 'ext.flutter.debugCheckElevationsEnabled', 325 ); 326 await _vmService.callServiceExtension( 327 'ext.flutter.debugCheckElevationsEnabled', 328 args: <dynamic, dynamic>{'enabled': !(response.json['enabled'] == 'true')}, 329 ); 330 } on vmservice.RPCError { 331 return; 332 } 333 } 334 335 @override 336 Future<void> debugTogglePerformanceOverlayOverride() async { 337 try { 338 final vmservice.Response response = await _vmService.callServiceExtension( 339 'ext.flutter.showPerformanceOverlay' 340 ); 341 await _vmService.callServiceExtension( 342 'ext.flutter.showPerformanceOverlay', 343 args: <dynamic, dynamic>{'enabled': !(response.json['enabled'] == 'true')}, 344 ); 345 } on vmservice.RPCError { 346 return; 347 } 348 } 349 350 @override 351 Future<void> debugToggleWidgetInspector() async { 352 try { 353 final vmservice.Response response = await _vmService.callServiceExtension( 354 'ext.flutter.debugToggleWidgetInspector' 355 ); 356 await _vmService.callServiceExtension( 357 'ext.flutter.debugToggleWidgetInspector', 358 args: <dynamic, dynamic>{'enabled': !(response.json['enabled'] == 'true')}, 359 ); 360 } on vmservice.RPCError { 361 return; 362 } 363 } 364 365 @override 366 Future<void> debugToggleProfileWidgetBuilds() async { 367 try { 368 final vmservice.Response response = await _vmService.callServiceExtension( 369 'ext.flutter.profileWidgetBuilds' 370 ); 371 await _vmService.callServiceExtension( 372 'ext.flutter.profileWidgetBuilds', 373 args: <dynamic, dynamic>{'enabled': !(response.json['enabled'] == 'true')}, 374 ); 375 } on vmservice.RPCError { 376 return; 377 } 378 } 379} 380