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:build_daemon/client.dart'; 8import 'package:build_daemon/constants.dart'; 9import 'package:build_daemon/constants.dart' hide BuildMode; 10import 'package:build_daemon/constants.dart' as daemon show BuildMode; 11import 'package:build_daemon/data/build_status.dart'; 12import 'package:build_daemon/data/build_target.dart'; 13import 'package:build_daemon/data/server_log.dart'; 14import 'package:dwds/dwds.dart'; 15import 'package:http_multi_server/http_multi_server.dart'; 16import 'package:meta/meta.dart'; 17import 'package:shelf/shelf.dart'; 18import 'package:shelf/shelf_io.dart' as shelf_io; 19import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart' hide StackTrace; 20 21import '../artifacts.dart'; 22import '../asset.dart'; 23import '../base/common.dart'; 24import '../base/context.dart'; 25import '../base/file_system.dart'; 26import '../base/io.dart'; 27import '../base/os.dart'; 28import '../build_info.dart'; 29import '../bundle.dart'; 30import '../cache.dart'; 31import '../globals.dart'; 32import '../project.dart'; 33import '../web/chrome.dart'; 34 35/// The name of the built web project. 36const String kBuildTargetName = 'web'; 37 38/// A factory for creating a [Dwds] instance. 39DwdsFactory get dwdsFactpory => context.get<DwdsFactory>() ?? Dwds.start; 40 41/// The [BuildDaemonCreator] instance. 42BuildDaemonCreator get buildDaemonCreator => context.get<BuildDaemonCreator>() ?? const BuildDaemonCreator(); 43 44/// A factory for creating a [WebFs] instance. 45WebFsFactory get webFsFactory => context.get<WebFsFactory>() ?? WebFs.start; 46 47/// A factory for creating an [HttpMultiServer] instance. 48HttpMultiServerFactory get httpMultiServerFactory => context.get<HttpMultiServerFactory>() ?? HttpMultiServer.bind; 49 50/// A function with the same signature as [HttpMultiServier.bind]. 51typedef HttpMultiServerFactory = Future<HttpServer> Function(dynamic address, int port); 52 53/// A function with the same signatire as [Dwds.start]. 54typedef DwdsFactory = Future<Dwds> Function({ 55 @required int applicationPort, 56 @required int assetServerPort, 57 @required String applicationTarget, 58 @required Stream<BuildResult> buildResults, 59 @required ConnectionProvider chromeConnection, 60 String hostname, 61 ReloadConfiguration reloadConfiguration, 62 bool serveDevTools, 63 LogWriter logWriter, 64 bool verbose, 65 bool enableDebugExtension, 66}); 67 68/// A function with the same signatuure as [WebFs.start]. 69typedef WebFsFactory = Future<WebFs> Function({ 70 @required String target, 71 @required FlutterProject flutterProject, 72 @required BuildInfo buildInfo, 73}); 74 75/// The dev filesystem responsible for building and serving web applications. 76class WebFs { 77 @visibleForTesting 78 WebFs( 79 this._client, 80 this._server, 81 this._dwds, 82 this._chrome, 83 ); 84 85 final HttpServer _server; 86 final Dwds _dwds; 87 final Chrome _chrome; 88 final BuildDaemonClient _client; 89 90 static const String _kHostName = 'localhost'; 91 92 Future<void> stop() async { 93 await _client.close(); 94 await _dwds.stop(); 95 await _server.close(force: true); 96 await _chrome.close(); 97 } 98 99 /// Retrieve the [DebugConnection] for the current application. 100 Future<DebugConnection> runAndDebug() async { 101 final AppConnection appConnection = await _dwds.connectedApps.first; 102 appConnection.runMain(); 103 return _dwds.debugConnection(appConnection); 104 } 105 106 /// Perform a hard refresh of all connected browser tabs. 107 Future<void> hardRefresh() async { 108 final List<ChromeTab> tabs = await _chrome.chromeConnection.getTabs(); 109 for (ChromeTab tab in tabs) { 110 if (!tab.url.contains('localhost')) { 111 continue; 112 } 113 final WipConnection connection = await tab.connect(); 114 await connection.sendCommand('Page.reload'); 115 } 116 } 117 118 /// Recompile the web application and return whether this was successful. 119 Future<bool> recompile() async { 120 _client.startBuild(); 121 await for (BuildResults results in _client.buildResults) { 122 final BuildResult result = results.results.firstWhere((BuildResult result) { 123 return result.target == 'web'; 124 }); 125 if (result.status == BuildStatus.failed) { 126 return false; 127 } 128 if (result.status == BuildStatus.succeeded) { 129 return true; 130 } 131 } 132 return true; 133 } 134 135 /// Start the web compiler and asset server. 136 static Future<WebFs> start({ 137 @required String target, 138 @required FlutterProject flutterProject, 139 @required BuildInfo buildInfo 140 }) async { 141 // Start the build daemon and run an initial build. 142 final BuildDaemonClient client = await buildDaemonCreator 143 .startBuildDaemon(fs.currentDirectory.path, release: buildInfo.isRelease); 144 client.startBuild(); 145 // Only provide relevant build results 146 final Stream<BuildResult> filteredBuildResults = client.buildResults 147 .asyncMap<BuildResult>((BuildResults results) { 148 return results.results 149 .firstWhere((BuildResult result) => result.target == kBuildTargetName); 150 }); 151 final int daemonAssetPort = buildDaemonCreator.assetServerPort(fs.currentDirectory); 152 153 // Initialize the asset bundle. 154 final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle(); 155 await assetBundle.build(); 156 await writeBundle(fs.directory(getAssetBuildDirectory()), assetBundle.entries); 157 158 // Initialize the dwds server. 159 final int port = await os.findFreePort(); 160 final Dwds dwds = await dwdsFactpory( 161 hostname: _kHostName, 162 applicationPort: port, 163 applicationTarget: kBuildTargetName, 164 assetServerPort: daemonAssetPort, 165 buildResults: filteredBuildResults, 166 chromeConnection: () async { 167 return (await ChromeLauncher.connectedInstance).chromeConnection; 168 }, 169 reloadConfiguration: ReloadConfiguration.none, 170 serveDevTools: true, 171 verbose: false, 172 enableDebugExtension: true, 173 logWriter: (dynamic level, String message) => printTrace(message), 174 ); 175 // Map the bootstrap files to the correct package directory. 176 final String targetBaseName = fs.path 177 .withoutExtension(target).replaceFirst('lib${fs.path.separator}', ''); 178 final Map<String, String> mappedUrls = <String, String>{ 179 'main.dart.js': 'packages/${flutterProject.manifest.appName}/' 180 '${targetBaseName}_web_entrypoint.dart.js', 181 '${targetBaseName}_web_entrypoint.dart.bootstrap.js': 'packages/${flutterProject.manifest.appName}/' 182 '${targetBaseName}_web_entrypoint.dart.bootstrap.js', 183 '${targetBaseName}_web_entrypoint.digests': 'packages/${flutterProject.manifest.appName}/' 184 '${targetBaseName}_web_entrypoint.digests', 185 }; 186 final Handler handler = const Pipeline().addMiddleware((Handler innerHandler) { 187 return (Request request) async { 188 // Redirect the main.dart.js to the target file we decided to serve. 189 if (mappedUrls.containsKey(request.url.path)) { 190 final String newPath = mappedUrls[request.url.path]; 191 return innerHandler( 192 Request( 193 request.method, 194 Uri.parse(request.requestedUri.toString() 195 .replaceFirst(request.requestedUri.path, '/$newPath')), 196 headers: request.headers, 197 url: Uri.parse(request.url.toString() 198 .replaceFirst(request.url.path, newPath)), 199 ), 200 ); 201 } else { 202 return innerHandler(request); 203 } 204 }; 205 }) 206 .addHandler(dwds.handler); 207 Cascade cascade = Cascade(); 208 cascade = cascade.add(handler); 209 cascade = cascade.add(_assetHandler); 210 final HttpServer server = await httpMultiServerFactory(_kHostName, port); 211 shelf_io.serveRequests(server, cascade.handler); 212 final Chrome chrome = await chromeLauncher.launch('http://$_kHostName:$port/'); 213 return WebFs( 214 client, 215 server, 216 dwds, 217 chrome, 218 ); 219 } 220 221 static Future<Response> _assetHandler(Request request) async { 222 if (request.url.path.contains('stack_trace_mapper')) { 223 final File file = fs.file(fs.path.join( 224 artifacts.getArtifactPath(Artifact.engineDartSdkPath), 225 'lib', 226 'dev_compiler', 227 'web', 228 'dart_stack_trace_mapper.js' 229 )); 230 return Response.ok(file.readAsBytesSync(), headers: <String, String>{ 231 'Content-Type': 'text/javascript', 232 }); 233 } else if (request.url.path.contains('require.js')) { 234 final File file = fs.file(fs.path.join( 235 artifacts.getArtifactPath(Artifact.engineDartSdkPath), 236 'lib', 237 'dev_compiler', 238 'kernel', 239 'amd', 240 'require.js' 241 )); 242 return Response.ok(file.readAsBytesSync(), headers: <String, String>{ 243 'Content-Type': 'text/javascript', 244 }); 245 } else if (request.url.path.contains('dart_sdk')) { 246 final File file = fs.file(fs.path.join( 247 artifacts.getArtifactPath(Artifact.flutterWebSdk), 248 'kernel', 249 'amd', 250 'dart_sdk.js', 251 )); 252 return Response.ok(file.readAsBytesSync(), headers: <String, String>{ 253 'Content-Type': 'text/javascript', 254 }); 255 } else if (request.url.path.contains('assets')) { 256 final String assetPath = request.url.path.replaceFirst('assets/', ''); 257 final File file = fs.file(fs.path.join(getAssetBuildDirectory(), assetPath)); 258 return Response.ok(file.readAsBytesSync()); 259 } 260 return Response.notFound(''); 261 } 262} 263 264/// A testable interface for starting a build daemon. 265class BuildDaemonCreator { 266 const BuildDaemonCreator(); 267 268 /// Start a build daemon and register the web targets. 269 Future<BuildDaemonClient> startBuildDaemon(String workingDirectory, {bool release = false}) async { 270 try { 271 final BuildDaemonClient client = await _connectClient( 272 workingDirectory, 273 release: release, 274 ); 275 _registerBuildTargets(client); 276 return client; 277 } on OptionsSkew { 278 throwToolExit( 279 'Incompatible options with current running build daemon.\n\n' 280 'Please stop other flutter_tool instances running in this directory ' 281 'before starting a new instance with these options.'); 282 } 283 return null; 284 } 285 286 void _registerBuildTargets( 287 BuildDaemonClient client, 288 ) { 289 final OutputLocation outputLocation = OutputLocation((OutputLocationBuilder b) => b 290 ..output = '' 291 ..useSymlinks = true 292 ..hoist = false); 293 client.registerBuildTarget(DefaultBuildTarget((DefaultBuildTargetBuilder b) => b 294 ..target = 'web' 295 ..outputLocation = outputLocation?.toBuilder())); 296 } 297 298 Future<BuildDaemonClient> _connectClient( 299 String workingDirectory, 300 { bool release } 301 ) { 302 final String flutterToolsPackages = fs.path.join(Cache.flutterRoot, 'packages', 'flutter_tools', '.packages'); 303 final String buildScript = fs.path.join(Cache.flutterRoot, 'packages', 'flutter_tools', 'lib', 'src', 'build_runner', 'build_script.dart'); 304 final String flutterWebSdk = artifacts.getArtifactPath(Artifact.flutterWebSdk); 305 return BuildDaemonClient.connect( 306 workingDirectory, 307 // On Windows we need to call the snapshot directly otherwise 308 // the process will start in a disjoint cmd without access to 309 // STDIO. 310 <String>[ 311 artifacts.getArtifactPath(Artifact.engineDartBinary), 312 '--packages=$flutterToolsPackages', 313 buildScript, 314 'daemon', 315 '--skip-build-script-check', 316 '--define', 'flutter_tools:ddc=flutterWebSdk=$flutterWebSdk', 317 '--define', 'flutter_tools:entrypoint=flutterWebSdk=$flutterWebSdk', 318 '--define', 'flutter_tools:entrypoint=release=$release', 319 '--define', 'flutter_tools:shell=flutterWebSdk=$flutterWebSdk', 320 ], 321 logHandler: (ServerLog serverLog) { 322 switch (serverLog.level) { 323 case Level.CONFIG: 324 case Level.FINE: 325 case Level.FINER: 326 case Level.FINEST: 327 case Level.INFO: 328 printTrace(serverLog.message); 329 break; 330 case Level.SEVERE: 331 case Level.SHOUT: 332 printError( 333 serverLog?.error ?? '', 334 stackTrace: serverLog.stackTrace != null 335 ? StackTrace.fromString(serverLog?.stackTrace) 336 : null, 337 ); 338 } 339 }, 340 buildMode: daemon.BuildMode.Manual, 341 ); 342 } 343 344 /// Retrieve the asset server port for the current daemon. 345 int assetServerPort(Directory workingDirectory) { 346 final String portFilePath = fs.path.join(daemonWorkspace(workingDirectory.path), '.asset_server_port'); 347 return int.tryParse(fs.file(portFilePath).readAsStringSync()); 348 } 349}