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

  @override
  Future<LaunchResult> startApp(
121
    ApplicationPackage? 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
    String url;
    if (debuggingOptions.webLaunchUrl != null) {
      final RegExp pattern = RegExp(r'^((http)?:\/\/)[^\s]+');
      if (pattern.hasMatch(debuggingOptions.webLaunchUrl!)) {
        url = debuggingOptions.webLaunchUrl!;
      } else {
Lioness100's avatar
Lioness100 committed
138
        throwToolExit('"${debuggingOptions.webLaunchUrl}" is not a valid HTTP URL.');
139 140 141 142
      }
    } 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
            .childDirectory('.dart_tool')
            .childDirectory('chrome-device'),
        headless: debuggingOptions.webRunHeadless,
        debugPort: debuggingOptions.webBrowserDebugPort,
152
        webBrowserFlags: debuggingOptions.webBrowserFlags,
153 154
      );
    }
155
    _logger.sendEvent('app.webLaunchUrl', <String, Object>{'url': url, 'launched': launchChrome});
156
    return LaunchResult.succeeded(vmServiceUri: Uri.parse(url));
157 158 159
  }

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

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

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

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

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

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

  final Platform _platform;
  final ProcessManager _processManager;

  @override
  String get name => 'Chrome';

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

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

258 259 260 261 262
  final ProcessManager _processManager;

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

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

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

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

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

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

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

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

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

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

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

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


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

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

397
  final Logger _logger;
398 399 400 401 402

  @override
  void clearLogs() { }

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

405
  DeviceLogReader? _logReader;
406

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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