• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}