// Copyright 2014 The Flutter 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:typed_data'; import 'package:meta/meta.dart'; import 'package:mime/mime.dart' as mime; import '../artifacts.dart'; import '../asset.dart'; import '../base/common.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/platform.dart'; import '../base/utils.dart'; import '../build_info.dart'; import '../bundle.dart'; import '../compile.dart'; import '../convert.dart'; import '../devfs.dart'; import '../globals.dart'; import 'bootstrap.dart'; /// A web server which handles serving JavaScript and assets. /// /// This is only used in development mode. class WebAssetServer { @visibleForTesting WebAssetServer(this._httpServer, { @required void Function(dynamic, StackTrace) onError }) { _httpServer.listen((HttpRequest request) { _handleRequest(request).catchError(onError); // TODO(jonahwilliams): test the onError callback when https://github.com/dart-lang/sdk/issues/39094 is fixed. }, onError: onError); } // Fallback to "application/octet-stream" on null which // makes no claims as to the structure of the data. static const String _kDefaultMimeType = 'application/octet-stream'; /// Start the web asset server on a [hostname] and [port]. /// /// Unhandled exceptions will throw a [ToolExit] with the error and stack /// trace. static Future<WebAssetServer> start(String hostname, int port) async { try { final HttpServer httpServer = await HttpServer.bind(hostname, port); return WebAssetServer(httpServer, onError: (dynamic error, StackTrace stackTrace) { httpServer.close(force: true); throwToolExit('Unhandled exception in web development server:\n$error\n$stackTrace'); }); } on SocketException catch (err) { throwToolExit('Failed to bind web development server:\n$err'); } assert(false); return null; } final HttpServer _httpServer; // If holding these in memory is too much overhead, this can be switched to a // RandomAccessFile and read on demand. final Map<String, Uint8List> _files = <String, Uint8List>{}; final Map<String, Uint8List> _sourcemaps = <String, Uint8List>{}; final RegExp _drivePath = RegExp(r'\/[A-Z]:\/'); // handle requests for JavaScript source, dart sources maps, or asset files. Future<void> _handleRequest(HttpRequest request) async { final HttpResponse response = request.response; // If the response is `/`, then we are requesting the index file. if (request.uri.path == '/') { final File indexFile = fs.currentDirectory .childDirectory('web') .childFile('index.html'); if (indexFile.existsSync()) { response.headers.add('Content-Type', 'text/html'); response.headers.add('Content-Length', indexFile.lengthSync()); await response.addStream(indexFile.openRead()); } else { response.statusCode = HttpStatus.notFound; } await response.close(); return; } // TODO(jonahwilliams): better path normalization in frontend_server to remove // this workaround. String requestPath = request.uri.path; if (requestPath.startsWith(_drivePath)) { requestPath = requestPath.substring(3); } // If this is a JavaScript file, it must be in the in-memory cache. // Attempt to look up the file by URI. if (_files.containsKey(requestPath)) { final List<int> bytes = _files[requestPath]; response.headers ..add('Content-Length', bytes.length) ..add('Content-Type', 'application/javascript'); response.add(bytes); await response.close(); return; } // If this is a sourcemap file, then it might be in the in-memory cache. // Attempt to lookup the file by URI. if (_sourcemaps.containsKey(requestPath)) { final List<int> bytes = _sourcemaps[requestPath]; response.headers ..add('Content-Length', bytes.length) ..add('Content-Type', 'application/json'); response.add(bytes); await response.close(); return; } // If this is a dart file, it must be on the local file system and is // likely coming from a source map request. Attempt to look in the // local filesystem for it, and return a 404 if it is not found. The tool // doesn't currently consider the case of Dart files as assets. File file = fs.file(Uri.base.resolve(request.uri.path)); // If both of the lookups above failed, the file might have been an asset. // Try and resolve the path relative to the built asset directory. if (!file.existsSync()) { final String assetPath = request.uri.path.replaceFirst('/assets/', ''); file = fs.file(fs.path.join(getAssetBuildDirectory(), fs.path.relative(assetPath))); } // If it isn't a project source or an asset, it must be a dart SDK source. // or a flutter web SDK source. if (!file.existsSync()) { final Directory dartSdkParent = fs.directory(artifacts.getArtifactPath(Artifact.engineDartSdkPath)).parent; file = fs.file(fs.path.joinAll(<String>[dartSdkParent.path, ...request.uri.pathSegments])); } if (!file.existsSync()) { final String flutterWebSdk = artifacts.getArtifactPath(Artifact.flutterWebSdk); file = fs.file(fs.path.joinAll(<String>[flutterWebSdk, ...request.uri.pathSegments])); } if (!file.existsSync()) { response.statusCode = HttpStatus.notFound; await response.close(); return; } final int length = file.lengthSync(); // Attempt to determine the file's mime type. if this is not provided some // browsers will refuse to render images/show video et cetera. If the tool // cannot determine a mime type, fall back to application/octet-stream. String mimeType; if (length >= 12) { mimeType= mime.lookupMimeType( file.path, headerBytes: await file.openRead(0, 12).first, ); } mimeType ??= _kDefaultMimeType; response.headers.add('Content-Length', length); response.headers.add('Content-Type', mimeType); await response.addStream(file.openRead()); await response.close(); } /// Tear down the http server running. Future<void> dispose() { return _httpServer.close(); } /// Write a single file into the in-memory cache. void writeFile(String filePath, String contents) { _files[filePath] = Uint8List.fromList(utf8.encode(contents)); } /// Update the in-memory asset server with the provided source and manifest files. /// /// Returns a list of updated modules. List<String> write(File codeFile, File manifestFile, File sourcemapFile) { final List<String> modules = <String>[]; final Uint8List codeBytes = codeFile.readAsBytesSync(); final Uint8List sourcemapBytes = sourcemapFile.readAsBytesSync(); final Map<String, dynamic> manifest = castStringKeyedMap(json.decode(manifestFile.readAsStringSync())); for (String filePath in manifest.keys) { if (filePath == null) { printTrace('Invalid manfiest file: $filePath'); continue; } final Map<String, dynamic> offsets = castStringKeyedMap(manifest[filePath]); final List<int> codeOffsets = (offsets['code'] as List<dynamic>).cast<int>(); final List<int> sourcemapOffsets = (offsets['sourcemap'] as List<dynamic>).cast<int>(); if (codeOffsets.length != 2 || sourcemapOffsets.length != 2) { printTrace('Invalid manifest byte offsets: $offsets'); continue; } final int codeStart = codeOffsets[0]; final int codeEnd = codeOffsets[1]; if (codeStart < 0 || codeEnd > codeBytes.lengthInBytes) { printTrace('Invalid byte index: [$codeStart, $codeEnd]'); continue; } final Uint8List byteView = Uint8List.view( codeBytes.buffer, codeStart, codeEnd - codeStart, ); _files[_filePathToUriFragment(filePath)] = byteView; final int sourcemapStart = sourcemapOffsets[0]; final int sourcemapEnd = sourcemapOffsets[1]; if (sourcemapStart < 0 || sourcemapEnd > sourcemapBytes.lengthInBytes) { printTrace('Invalid byte index: [$sourcemapStart, $sourcemapEnd]'); continue; } final Uint8List sourcemapView = Uint8List.view( sourcemapBytes.buffer, sourcemapStart, sourcemapEnd - sourcemapStart , ); _sourcemaps['${_filePathToUriFragment(filePath)}.map'] = sourcemapView; modules.add(filePath); } return modules; } } class WebDevFS implements DevFS { WebDevFS(this.hostname, this.port, this._packagesFilePath); final String hostname; final int port; final String _packagesFilePath; WebAssetServer _webAssetServer; @override List<Uri> sources = <Uri>[]; @override DateTime lastCompiled; // We do not evict assets on the web. @override Set<String> get assetPathsToEvict => const <String>{}; @override Uri get baseUri => null; @override Future<Uri> create() async { _webAssetServer = await WebAssetServer.start(hostname, port); return Uri.base; } @override Future<void> destroy() async { await _webAssetServer.dispose(); } @override Uri deviceUriToHostUri(Uri deviceUri) { return deviceUri; } @override String get fsName => 'web_asset'; @override Directory get rootDirectory => null; @override Future<UpdateFSReport> update({ String mainPath, String target, AssetBundle bundle, DateTime firstBuildTime, bool bundleFirstUpload = false, @required ResidentCompiler generator, String dillOutputPath, @required bool trackWidgetCreation, bool fullRestart = false, String projectRootPath, String pathToReload, List<Uri> invalidatedFiles, bool skipAssets = false, }) async { assert(trackWidgetCreation != null); assert(generator != null); if (bundleFirstUpload) { final File requireJS = fs.file(fs.path.join( artifacts.getArtifactPath(Artifact.engineDartSdkPath), 'lib', 'dev_compiler', 'kernel', 'amd', 'require.js', )); final File dartSdk = fs.file(fs.path.join( artifacts.getArtifactPath(Artifact.flutterWebSdk), 'kernel', 'amd', 'dart_sdk.js', )); final File dartSdkSourcemap = fs.file(fs.path.join( artifacts.getArtifactPath(Artifact.flutterWebSdk), 'kernel', 'amd', 'dart_sdk.js.map', )); final File stackTraceMapper = fs.file(fs.path.join( artifacts.getArtifactPath(Artifact.engineDartSdkPath), 'lib', 'dev_compiler', 'web', 'dart_stack_trace_mapper.js', )); _webAssetServer.writeFile('/main.dart.js', generateBootstrapScript( requireUrl: _filePathToUriFragment(requireJS.path), mapperUrl: _filePathToUriFragment(stackTraceMapper.path), entrypoint: '${_filePathToUriFragment(mainPath)}.js', )); _webAssetServer.writeFile('/main_module.js', generateMainModule( entrypoint: '${_filePathToUriFragment(mainPath)}.js', )); _webAssetServer.writeFile('/dart_sdk.js', dartSdk.readAsStringSync()); _webAssetServer.writeFile('/dart_sdk.js.map', dartSdkSourcemap.readAsStringSync()); // TODO(jonahwilliams): refactor the asset code in this and the regular devfs to // be shared. await writeBundle(fs.directory(getAssetBuildDirectory()), bundle.entries); } final DateTime candidateCompileTime = DateTime.now(); if (fullRestart) { generator.reset(); } final CompilerOutput compilerOutput = await generator.recompile( mainPath, invalidatedFiles, outputPath: dillOutputPath ?? getDefaultApplicationKernelPath(trackWidgetCreation: trackWidgetCreation), packagesFilePath : _packagesFilePath, ); if (compilerOutput == null || compilerOutput.errorCount > 0) { return UpdateFSReport(success: false); } // Only update the last compiled time if we successfully compiled. lastCompiled = candidateCompileTime; // list of sources that needs to be monitored are in [compilerOutput.sources] sources = compilerOutput.sources; File codeFile; File manifestFile; File sourcemapFile; List<String> modules; try { codeFile = fs.file('${compilerOutput.outputFilename}.sources'); manifestFile = fs.file('${compilerOutput.outputFilename}.json'); sourcemapFile = fs.file('${compilerOutput.outputFilename}.map'); modules = _webAssetServer.write(codeFile, manifestFile, sourcemapFile); } on FileSystemException catch (err) { throwToolExit('Failed to load recompiled sources:\n$err'); } return UpdateFSReport(success: true, syncedBytes: codeFile.lengthSync(), invalidatedSourcesCount: invalidatedFiles.length) ..invalidatedModules = modules.map(_filePathToUriFragment).toList(); } } String _filePathToUriFragment(String path) { if (platform.isWindows) { final bool startWithSlash = path.startsWith('/'); final String partial = fs.path .split(path) .skip(startWithSlash ? 2 : 1).join('/'); if (partial.startsWith('/')) { return partial; } return '/$partial'; } return path; }