web_device.dart 13.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:meta/meta.dart';
6
import 'package:process/process.dart';
7

8 9 10
import '../application_package.dart';
import '../base/file_system.dart';
import '../base/io.dart';
11 12
import '../base/logger.dart';
import '../base/os.dart';
13
import '../base/platform.dart';
14
import '../base/version.dart';
15 16
import '../build_info.dart';
import '../device.dart';
17
import '../features.dart';
18
import '../project.dart';
19
import 'chrome.dart';
20 21

class WebApplicationPackage extends ApplicationPackage {
22
  WebApplicationPackage(this.flutterProject) : super(id: flutterProject.manifest.appName);
23

24
  final FlutterProject flutterProject;
25 26

  @override
27
  String get name => flutterProject.manifest.appName;
28 29

  /// The location of the web source assets.
30
  Directory get webSourcePath => flutterProject.directory.childDirectory('web');
31 32
}

33 34 35 36 37 38
/// A web device that supports a chromium browser.
abstract class ChromiumDevice extends Device {
  ChromiumDevice({
    @required String name,
    @required this.chromeLauncher,
    @required FileSystem fileSystem,
39
    @required Logger logger,
40 41 42 43 44 45 46 47 48 49 50 51
  }) : _fileSystem = fileSystem,
       _logger = logger,
       super(
         name,
         category: Category.web,
         platformType: PlatformType.web,
         ephemeral: false,
       );

  final ChromiumLauncher chromeLauncher;

  final FileSystem _fileSystem;
52
  final Logger _logger;
53

54
  /// The active chrome instance.
55
  Chromium _chrome;
56

57 58
  // TODO(jonahwilliams): this is technically false, but requires some refactoring
  // to allow hot mode restart only devices.
59
  @override
60
  bool get supportsHotReload => true;
61 62

  @override
63
  bool get supportsHotRestart => true;
64 65 66 67 68

  @override
  bool get supportsStartPaused => true;

  @override
69
  bool get supportsFlutterExit => false;
70 71 72 73

  @override
  bool get supportsScreenshot => false;

74 75 76
  @override
  bool supportsRuntimeMode(BuildMode buildMode) => buildMode != BuildMode.jitRelease;

77
  @override
78
  void clearLogs() { }
79

80 81
  DeviceLogReader _logReader;

82
  @override
83 84 85 86
  DeviceLogReader getLogReader({
    ApplicationPackage app,
    bool includePastLogs = false,
  }) {
87
    return _logReader ??= NoOpDeviceLogReader(app?.name);
88 89 90
  }

  @override
91 92 93 94
  Future<bool> installApp(
    ApplicationPackage app, {
    String userIdentifier,
  }) async => true;
95 96

  @override
97 98 99 100
  Future<bool> isAppInstalled(
    ApplicationPackage app, {
    String userIdentifier,
  }) async => true;
101 102 103 104 105 106 107

  @override
  Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => true;

  @override
  Future<bool> get isLocalEmulator async => false;

108 109 110
  @override
  Future<String> get emulatorId async => null;

111
  @override
112
  bool isSupported() =>  chromeLauncher.canFindExecutable();
113 114 115 116 117 118 119 120 121 122 123 124 125

  @override
  DevicePortForwarder get portForwarder => const NoOpDevicePortForwarder();

  @override
  Future<LaunchResult> startApp(
    covariant WebApplicationPackage package, {
    String mainPath,
    String route,
    DebuggingOptions debuggingOptions,
    Map<String, Object> platformArgs,
    bool prebuiltApplication = false,
    bool ipv6 = false,
126
    String userIdentifier,
127
  }) async {
128 129
    // See [ResidentWebRunner.run] in flutter_tools/lib/src/resident_web_runner.dart
    // for the web initialization and server logic.
130
    final String url = platformArgs['uri'] as String;
131 132
    final bool launchChrome = platformArgs['no-launch-chrome'] != true;
    if (launchChrome) {
133
      _chrome = await chromeLauncher.launch(
134
        url,
135
        cacheDir: _fileSystem.currentDirectory
136 137 138 139 140 141
            .childDirectory('.dart_tool')
            .childDirectory('chrome-device'),
        headless: debuggingOptions.webRunHeadless,
        debugPort: debuggingOptions.webBrowserDebugPort,
      );
    }
142
    _logger.sendEvent('app.webLaunchUrl', <String, dynamic>{'url': url, 'launched': launchChrome});
143
    return LaunchResult.succeeded(observatoryUri: url != null ? Uri.parse(url): null);
144 145 146
  }

  @override
147 148 149 150
  Future<bool> stopApp(
    ApplicationPackage app, {
    String userIdentifier,
  }) async {
151
    await _chrome?.close();
152 153 154 155
    return true;
  }

  @override
156
  Future<TargetPlatform> get targetPlatform async => TargetPlatform.web_javascript;
157 158

  @override
159 160 161 162
  Future<bool> uninstallApp(
    ApplicationPackage app, {
    String userIdentifier,
  }) async => true;
163

164 165 166 167
  @override
  bool isSupportedForProject(FlutterProject flutterProject) {
    return flutterProject.web.existsSync();
  }
168 169 170 171 172 173

  @override
  Future<void> dispose() async {
    _logReader?.dispose();
    await portForwarder?.dispose();
  }
174 175
}

176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
/// The Google Chrome browser based on Chromium.
class GoogleChromeDevice extends ChromiumDevice {
  GoogleChromeDevice({
    @required Platform platform,
    @required ProcessManager processManager,
    @required ChromiumLauncher chromiumLauncher,
    @required Logger logger,
    @required FileSystem fileSystem,
  }) : _platform = platform,
       _processManager = processManager,
       super(
          name: 'chrome',
          chromeLauncher: chromiumLauncher,
          logger: logger,
          fileSystem: fileSystem,
       );

  final Platform _platform;
  final ProcessManager _processManager;

  @override
  String get name => 'Chrome';

  @override
  Future<String> get sdkNameAndVersion async => _sdkNameAndVersion ??= await _computeSdkNameAndVersion();

  String _sdkNameAndVersion;
  Future<String> _computeSdkNameAndVersion() async {
    if (!isSupported()) {
      return 'unknown';
    }
    // See https://bugs.chromium.org/p/chromium/issues/detail?id=158372
    String version = 'unknown';
    if (_platform.isWindows) {
      final ProcessResult result = await _processManager.run(<String>[
        r'reg', 'query', r'HKEY_CURRENT_USER\Software\Google\Chrome\BLBeacon', '/v', 'version',
      ]);
      if (result.exitCode == 0) {
        final List<String> parts = (result.stdout as String).split(RegExp(r'\s+'));
        if (parts.length > 2) {
          version = 'Google Chrome ' + parts[parts.length - 2];
        }
      }
    } else {
      final String chrome = chromeLauncher.findExecutable();
      final ProcessResult result = await _processManager.run(<String>[
        chrome,
        '--version',
      ]);
      if (result.exitCode == 0) {
        version = result.stdout as String;
      }
    }
    return version.trim();
  }

}

/// The Microsoft Edge browser based on Chromium.
class MicrosoftEdgeDevice extends ChromiumDevice {
  MicrosoftEdgeDevice({
    @required ChromiumLauncher chromiumLauncher,
    @required Logger logger,
    @required FileSystem fileSystem,
240 241 242
    @required ProcessManager processManager,
  }) : _processManager = processManager,
       super(
243 244 245 246 247 248
         name: 'edge',
         chromeLauncher: chromiumLauncher,
         logger: logger,
         fileSystem: fileSystem,
       );

249 250 251 252 253
  final ProcessManager _processManager;

  // The first version of Edge with chromium support.
  static const int _kFirstChromiumEdgeMajorVersion = 79;

254 255 256
  @override
  String get name => 'Edge';

257 258 259 260 261 262 263 264 265
  Future<bool> _meetsVersionConstraint() async {
    final String rawVersion = (await sdkNameAndVersion).replaceFirst('Microsoft Edge ', '');
    final Version version = Version.parse(rawVersion);
    if (version == null) {
      return false;
    }
    return version.major >= _kFirstChromiumEdgeMajorVersion;
  }

266
  @override
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
  Future<String> get sdkNameAndVersion async => _sdkNameAndVersion ??= await _getSdkNameAndVersion();
  String _sdkNameAndVersion;
  Future<String> _getSdkNameAndVersion() async {
    final ProcessResult result = await _processManager.run(<String>[
      r'reg', 'query', r'HKEY_CURRENT_USER\Software\Microsoft\Edge\BLBeacon', '/v', 'version',
    ]);
    if (result.exitCode == 0) {
      final List<String> parts = (result.stdout as String).split(RegExp(r'\s+'));
      if (parts.length > 2) {
        return 'Microsoft Edge ' + parts[parts.length - 2];
      }
    }
    // Return a non-null string so that the tool can validate the version
    // does not meet the constraint above in _meetsVersionConstraint.
    return '';
  }
283 284
}

285
class WebDevices extends PollingDeviceDiscovery {
286 287
  WebDevices({
    @required FileSystem fileSystem,
288
    @required Logger logger,
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
    @required Platform platform,
    @required ProcessManager processManager,
    @required FeatureFlags featureFlags,
  }) : _featureFlags = featureFlags,
       super('Chrome') {
    final OperatingSystemUtils operatingSystemUtils = OperatingSystemUtils(
      fileSystem: fileSystem,
      platform: platform,
      logger: logger,
      processManager: processManager,
    );
    _chromeDevice = GoogleChromeDevice(
      fileSystem: fileSystem,
      logger: logger,
      platform: platform,
      processManager: processManager,
      chromiumLauncher: ChromiumLauncher(
        browserFinder: findChromeExecutable,
        fileSystem: fileSystem,
        platform: platform,
        processManager: processManager,
        operatingSystemUtils: operatingSystemUtils,
311
        logger: logger,
312 313
      ),
    );
314 315 316 317 318 319 320 321
    if (platform.isWindows) {
      _edgeDevice = MicrosoftEdgeDevice(
        chromiumLauncher: ChromiumLauncher(
          browserFinder: findEdgeExecutable,
          fileSystem: fileSystem,
          platform: platform,
          processManager: processManager,
          operatingSystemUtils: operatingSystemUtils,
322
          logger: logger,
323 324 325 326 327 328
        ),
        processManager: processManager,
        logger: logger,
        fileSystem: fileSystem,
      );
    }
329 330 331 332
    _webServerDevice = WebServerDevice(
      logger: logger,
    );
  }
333

334 335
  GoogleChromeDevice _chromeDevice;
  WebServerDevice _webServerDevice;
336
  MicrosoftEdgeDevice _edgeDevice;
337
  final FeatureFlags _featureFlags;
338 339

  @override
340
  bool get canListAnything => featureFlags.isWebEnabled;
341 342

  @override
343
  Future<List<Device>> pollingGetDevices({ Duration timeout }) async {
344 345 346
    if (!_featureFlags.isWebEnabled) {
      return <Device>[];
    }
347
    return <Device>[
348
      _webServerDevice,
349 350
      if (_chromeDevice.isSupported())
        _chromeDevice,
351 352
      if (await _edgeDevice?._meetsVersionConstraint() ?? false)
        _edgeDevice,
353 354 355 356
    ];
  }

  @override
357
  bool get supportsPlatform =>  _featureFlags.isWebEnabled;
358
}
359 360 361

@visibleForTesting
String parseVersionForWindows(String input) {
362
  return input.split(RegExp(r'\w')).last;
363
}
364 365 366 367


/// A special device type to allow serving for arbitrary browsers.
class WebServerDevice extends Device {
368
  WebServerDevice({
369
    @required Logger logger,
370 371 372 373 374 375 376 377
  }) : _logger = logger,
       super(
         'web-server',
          platformType: PlatformType.web,
          category: Category.web,
          ephemeral: false,
       );

378
  final Logger _logger;
379 380 381 382 383 384 385

  @override
  void clearLogs() { }

  @override
  Future<String> get emulatorId => null;

386 387
  DeviceLogReader _logReader;

388
  @override
389 390 391 392
  DeviceLogReader getLogReader({
    ApplicationPackage app,
    bool includePastLogs = false,
  }) {
393
    return _logReader ??= NoOpDeviceLogReader(app?.name);
394 395 396
  }

  @override
397 398 399 400
  Future<bool> installApp(
    ApplicationPackage app, {
    String userIdentifier,
  }) async => true;
401 402

  @override
403 404 405 406
  Future<bool> isAppInstalled(
    ApplicationPackage app, {
    String userIdentifier,
  }) async => true;
407 408 409 410

  @override
  Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => true;

411 412 413
  @override
  bool get supportsFlutterExit => false;

414 415 416
  @override
  bool supportsRuntimeMode(BuildMode buildMode) => buildMode != BuildMode.jitRelease;

417 418 419 420
  @override
  Future<bool> get isLocalEmulator async => false;

  @override
421
  bool isSupported() => true;
422 423 424 425 426 427 428

  @override
  bool isSupportedForProject(FlutterProject flutterProject) {
    return flutterProject.web.existsSync();
  }

  @override
429
  String get name => 'Web Server';
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444

  @override
  DevicePortForwarder get portForwarder => const NoOpDevicePortForwarder();

  @override
  Future<String> get sdkNameAndVersion async => 'Flutter Tools';

  @override
  Future<LaunchResult> startApp(ApplicationPackage package, {
    String mainPath,
    String route,
    DebuggingOptions debuggingOptions,
    Map<String, Object> platformArgs,
    bool prebuiltApplication = false,
    bool ipv6 = false,
445
    String userIdentifier,
446
  }) async {
447
    final String url = platformArgs['uri'] as String;
448
    if (debuggingOptions.startPaused) {
449
      _logger.printStatus('Waiting for connection from Dart debug extension at $url', emphasis: true);
450
    } else {
451
      _logger.printStatus('$mainPath is being served at $url', emphasis: true);
452
    }
453
    _logger.printStatus(
454 455
      'The web-server device requires the Dart Debug Chrome extension for debugging. '
      'Consider using the Chrome or Edge devices for an improved development workflow.'
456
    );
457
    _logger.sendEvent('app.webLaunchUrl', <String, dynamic>{'url': url, 'launched': false});
458
    return LaunchResult.succeeded(observatoryUri: url != null ? Uri.parse(url): null);
459 460 461
  }

  @override
462 463 464 465
  Future<bool> stopApp(
    ApplicationPackage app, {
    String userIdentifier,
  }) async {
466 467 468 469 470 471 472
    return true;
  }

  @override
  Future<TargetPlatform> get targetPlatform async => TargetPlatform.web_javascript;

  @override
473 474 475 476
  Future<bool> uninstallApp(
    ApplicationPackage app, {
    String userIdentifier,
  }) async {
477 478
    return true;
  }
479 480 481 482 483 484

  @override
  Future<void> dispose() async {
    _logReader?.dispose();
    await portForwarder?.dispose();
  }
485
}