web_device.dart 13.9 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
import '../application_package.dart';
9
import '../base/common.dart';
10 11
import '../base/file_system.dart';
import '../base/io.dart';
12 13
import '../base/logger.dart';
import '../base/os.dart';
14
import '../base/platform.dart';
15
import '../base/version.dart';
16 17
import '../build_info.dart';
import '../device.dart';
18
import '../device_port_forwarder.dart';
19
import '../features.dart';
20
import '../project.dart';
21
import 'chrome.dart';
22 23

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

26
  final FlutterProject flutterProject;
27 28

  @override
29
  String get name => flutterProject.manifest.appName;
30 31

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

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

  final ChromiumLauncher chromeLauncher;

  final FileSystem _fileSystem;
54
  final Logger _logger;
55

56
  /// The active chrome instance.
57
  Chromium? _chrome;
58

59 60
  // This device does not actually support hot reload, but the current implementation of the resident runner
  // requires both supportsHotReload and supportsHotRestart to be true in order to allow hot restart.
61
  @override
62
  bool get supportsHotReload => true;
63 64

  @override
65
  bool get supportsHotRestart => true;
66 67 68 69 70

  @override
  bool get supportsStartPaused => true;

  @override
71
  bool get supportsFlutterExit => false;
72 73 74 75

  @override
  bool get supportsScreenshot => false;

76 77 78
  @override
  bool supportsRuntimeMode(BuildMode buildMode) => buildMode != BuildMode.jitRelease;

79
  @override
80
  void clearLogs() { }
81

82
  DeviceLogReader? _logReader;
83

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

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

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

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

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

110
  @override
111
  Future<String?> get emulatorId async => null;
112

113
  @override
114
  bool isSupported() =>  chromeLauncher.canFindExecutable();
115 116

  @override
117
  DevicePortForwarder? get portForwarder => const NoOpDevicePortForwarder();
118 119 120 121

  @override
  Future<LaunchResult> startApp(
    covariant WebApplicationPackage package, {
122 123 124 125
    String? mainPath,
    String? route,
    required DebuggingOptions debuggingOptions,
    Map<String, Object?> platformArgs = const <String, Object?>{},
126 127
    bool prebuiltApplication = false,
    bool ipv6 = false,
128
    String? userIdentifier,
129
  }) async {
130 131
    // See [ResidentWebRunner.run] in flutter_tools/lib/src/resident_web_runner.dart
    // for the web initialization and server logic.
132 133 134 135 136 137 138 139 140 141 142
    String url;
    if (debuggingOptions.webLaunchUrl != null) {
      final RegExp pattern = RegExp(r'^((http)?:\/\/)[^\s]+');
      if (pattern.hasMatch(debuggingOptions.webLaunchUrl!)) {
        url = debuggingOptions.webLaunchUrl!;
      } else {
        throwToolExit('"${debuggingOptions.webLaunchUrl}" is not a vaild HTTP URL.');
      }
    } else {
      url = platformArgs['uri']! as String;
    }
143 144
    final bool launchChrome = platformArgs['no-launch-chrome'] != true;
    if (launchChrome) {
145
      _chrome = await chromeLauncher.launch(
146
        url,
147
        cacheDir: _fileSystem.currentDirectory
148 149 150 151 152 153
            .childDirectory('.dart_tool')
            .childDirectory('chrome-device'),
        headless: debuggingOptions.webRunHeadless,
        debugPort: debuggingOptions.webBrowserDebugPort,
      );
    }
154
    _logger.sendEvent('app.webLaunchUrl', <String, Object>{'url': url, 'launched': launchChrome});
155
    return LaunchResult.succeeded(observatoryUri: url != null ? Uri.parse(url): null);
156 157 158
  }

  @override
159 160
  Future<bool> stopApp(
    ApplicationPackage app, {
161
    String? userIdentifier,
162
  }) async {
163
    await _chrome?.close();
164 165 166 167
    return true;
  }

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

  @override
171 172
  Future<bool> uninstallApp(
    ApplicationPackage app, {
173
    String? userIdentifier,
174
  }) async => true;
175

176 177 178 179
  @override
  bool isSupportedForProject(FlutterProject flutterProject) {
    return flutterProject.web.existsSync();
  }
180 181 182 183 184 185

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

188 189 190
/// The Google Chrome browser based on Chromium.
class GoogleChromeDevice extends ChromiumDevice {
  GoogleChromeDevice({
191 192 193
    required Platform platform,
    required ProcessManager processManager,
    required ChromiumLauncher chromiumLauncher,
194 195
    required super.logger,
    required super.fileSystem,
196 197 198 199 200 201 202 203 204 205 206 207 208 209
  }) : _platform = platform,
       _processManager = processManager,
       super(
          name: 'chrome',
          chromeLauncher: chromiumLauncher,
       );

  final Platform _platform;
  final ProcessManager _processManager;

  @override
  String get name => 'Chrome';

  @override
210
  late final Future<String> sdkNameAndVersion = _computeSdkNameAndVersion();
211 212 213 214 215 216 217 218

  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) {
219 220 221 222 223 224 225
      if (_processManager.canRun('reg')) {
        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) {
226
            version = 'Google Chrome ${parts[parts.length - 2]}';
227
          }
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
        }
      }
    } 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({
247
    required ChromiumLauncher chromiumLauncher,
248 249
    required super.logger,
    required super.fileSystem,
250
    required ProcessManager processManager,
251 252
  }) : _processManager = processManager,
       super(
253 254 255 256
         name: 'edge',
         chromeLauncher: chromiumLauncher,
       );

257 258 259 260 261
  final ProcessManager _processManager;

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

262 263 264
  @override
  String get name => 'Edge';

265 266
  Future<bool> _meetsVersionConstraint() async {
    final String rawVersion = (await sdkNameAndVersion).replaceFirst('Microsoft Edge ', '');
267
    final Version? version = Version.parse(rawVersion);
268 269 270 271 272 273
    if (version == null) {
      return false;
    }
    return version.major >= _kFirstChromiumEdgeMajorVersion;
  }

274
  @override
275 276
  late final Future<String> sdkNameAndVersion = _getSdkNameAndVersion();

277
  Future<String> _getSdkNameAndVersion() async {
278 279 280 281 282 283 284
    if (_processManager.canRun('reg')) {
      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) {
285
          return 'Microsoft Edge ${parts[parts.length - 2]}';
286
        }
287 288 289 290 291 292
      }
    }
    // Return a non-null string so that the tool can validate the version
    // does not meet the constraint above in _meetsVersionConstraint.
    return '';
  }
293 294
}

295
class WebDevices extends PollingDeviceDiscovery {
296
  WebDevices({
297 298 299 300 301
    required FileSystem fileSystem,
    required Logger logger,
    required Platform platform,
    required ProcessManager processManager,
    required FeatureFlags featureFlags,
302
  }) : _featureFlags = featureFlags,
303 304 305
       _webServerDevice = WebServerDevice(
         logger: logger,
       ),
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
       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,
324
        logger: logger,
325 326
      ),
    );
327 328 329 330 331 332 333 334
    if (platform.isWindows) {
      _edgeDevice = MicrosoftEdgeDevice(
        chromiumLauncher: ChromiumLauncher(
          browserFinder: findEdgeExecutable,
          fileSystem: fileSystem,
          platform: platform,
          processManager: processManager,
          operatingSystemUtils: operatingSystemUtils,
335
          logger: logger,
336 337 338 339 340 341
        ),
        processManager: processManager,
        logger: logger,
        fileSystem: fileSystem,
      );
    }
342
  }
343

344 345 346
  late final GoogleChromeDevice _chromeDevice;
  final WebServerDevice _webServerDevice;
  MicrosoftEdgeDevice? _edgeDevice;
347
  final FeatureFlags _featureFlags;
348 349

  @override
350
  bool get canListAnything => featureFlags.isWebEnabled;
351 352

  @override
353
  Future<List<Device>> pollingGetDevices({ Duration? timeout }) async {
354 355 356
    if (!_featureFlags.isWebEnabled) {
      return <Device>[];
    }
357
    final MicrosoftEdgeDevice? edgeDevice = _edgeDevice;
358
    return <Device>[
359 360
      if (WebServerDevice.showWebServerDevice)
        _webServerDevice,
361 362
      if (_chromeDevice.isSupported())
        _chromeDevice,
363 364
      if (edgeDevice != null && await edgeDevice._meetsVersionConstraint())
        edgeDevice,
365 366 367 368
    ];
  }

  @override
369
  bool get supportsPlatform =>  _featureFlags.isWebEnabled;
370 371 372

  @override
  List<String> get wellKnownIds => const <String>['chrome', 'web-server', 'edge'];
373
}
374 375 376

@visibleForTesting
String parseVersionForWindows(String input) {
377
  return input.split(RegExp(r'\w')).last;
378
}
379 380 381 382


/// A special device type to allow serving for arbitrary browsers.
class WebServerDevice extends Device {
383
  WebServerDevice({
384
    required Logger logger,
385 386 387 388 389 390 391 392
  }) : _logger = logger,
       super(
         'web-server',
          platformType: PlatformType.web,
          category: Category.web,
          ephemeral: false,
       );

393
  static const String kWebServerDeviceId = 'web-server';
394
  static bool showWebServerDevice = false;
395

396
  final Logger _logger;
397 398 399 400 401

  @override
  void clearLogs() { }

  @override
402
  Future<String?> get emulatorId async => null;
403

404
  DeviceLogReader? _logReader;
405

406
  @override
407
  DeviceLogReader getLogReader({
408
    ApplicationPackage? app,
409 410
    bool includePastLogs = false,
  }) {
411
    return _logReader ??= NoOpDeviceLogReader(app?.name);
412 413 414
  }

  @override
415 416
  Future<bool> installApp(
    ApplicationPackage app, {
417
    String? userIdentifier,
418
  }) async => true;
419 420

  @override
421 422
  Future<bool> isAppInstalled(
    ApplicationPackage app, {
423
    String? userIdentifier,
424
  }) async => true;
425 426 427 428

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

429 430 431
  @override
  bool get supportsFlutterExit => false;

432 433 434
  @override
  bool supportsRuntimeMode(BuildMode buildMode) => buildMode != BuildMode.jitRelease;

435 436 437 438
  @override
  Future<bool> get isLocalEmulator async => false;

  @override
439
  bool isSupported() => true;
440 441 442 443 444 445 446

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

  @override
447
  String get name => 'Web Server';
448 449

  @override
450
  DevicePortForwarder? get portForwarder => const NoOpDevicePortForwarder();
451 452 453 454 455 456

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

  @override
  Future<LaunchResult> startApp(ApplicationPackage package, {
457 458 459 460
    String? mainPath,
    String? route,
    required DebuggingOptions debuggingOptions,
    Map<String, Object?> platformArgs = const <String, Object?>{},
461 462
    bool prebuiltApplication = false,
    bool ipv6 = false,
463
    String? userIdentifier,
464
  }) async {
465
    final String? url = platformArgs['uri'] as String?;
466
    if (debuggingOptions.startPaused) {
467
      _logger.printStatus('Waiting for connection from Dart debug extension at $url', emphasis: true);
468
    } else {
469
      _logger.printStatus('$mainPath is being served at $url', emphasis: true);
470
    }
471
    _logger.printStatus(
472 473
      'The web-server device requires the Dart Debug Chrome extension for debugging. '
      'Consider using the Chrome or Edge devices for an improved development workflow.'
474
    );
475
    _logger.sendEvent('app.webLaunchUrl', <String, Object?>{'url': url, 'launched': false});
476
    return LaunchResult.succeeded(observatoryUri: url != null ? Uri.parse(url): null);
477 478 479
  }

  @override
480 481
  Future<bool> stopApp(
    ApplicationPackage app, {
482
    String? userIdentifier,
483
  }) async {
484 485 486 487 488 489 490
    return true;
  }

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

  @override
491 492
  Future<bool> uninstallApp(
    ApplicationPackage app, {
493
    String? userIdentifier,
494
  }) async {
495 496
    return true;
  }
497 498 499 500 501 502

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