simulators.dart 29.5 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 6
// @dart = 2.8

7
import 'dart:async';
8
import 'dart:math' as math;
9

10
import 'package:meta/meta.dart';
11
import 'package:process/process.dart';
12

13
import '../application_package.dart';
14
import '../base/common.dart';
15
import '../base/file_system.dart';
16
import '../base/io.dart';
17
import '../base/logger.dart';
18
import '../base/process.dart';
19
import '../base/utils.dart';
20
import '../build_info.dart';
21
import '../convert.dart';
22
import '../devfs.dart';
23
import '../device.dart';
24
import '../globals.dart' as globals;
25
import '../macos/xcode.dart';
26
import '../project.dart';
27
import '../protocol_discovery.dart';
28
import 'mac.dart';
29
import 'plist_parser.dart';
30

31
const String iosSimulatorId = 'apple_ios_simulator';
32 33

class IOSSimulators extends PollingDeviceDiscovery {
34 35 36 37 38 39
  IOSSimulators({
    @required IOSSimulatorUtils iosSimulatorUtils,
  }) : _iosSimulatorUtils = iosSimulatorUtils,
       super('iOS simulators');

  final IOSSimulatorUtils _iosSimulatorUtils;
40

41
  @override
42
  bool get supportsPlatform => globals.platform.isMacOS;
43

44
  @override
45
  bool get canListAnything => globals.iosWorkflow.canListDevices;
46

47
  @override
48
  Future<List<Device>> pollingGetDevices({ Duration timeout }) async => _iosSimulatorUtils.getAttachedDevices();
49 50 51
}

class IOSSimulatorUtils {
52 53
  IOSSimulatorUtils({
    @required Xcode xcode,
54 55
    @required Logger logger,
    @required ProcessManager processManager,
56 57 58 59 60 61
  })  : _simControl = SimControl(
          logger: logger,
          processManager: processManager,
          xcode: xcode,
        ),
        _xcode = xcode;
62 63 64

  final SimControl _simControl;
  final Xcode _xcode;
65

66
  Future<List<IOSSimulator>> getAttachedDevices() async {
67
    if (!_xcode.isInstalledAndMeetsVersionCheck) {
68
      return <IOSSimulator>[];
69
    }
70

71
    final List<SimDevice> connected = await _simControl.getConnectedDevices();
72
    return connected.map<IOSSimulator>((SimDevice device) {
73 74 75 76 77 78
      return IOSSimulator(
        device.udid,
        name: device.name,
        simControl: _simControl,
        simulatorCategory: device.category,
      );
79 80
    }).toList();
  }
81 82 83 84
}

/// A wrapper around the `simctl` command line tool.
class SimControl {
85 86 87
  SimControl({
    @required Logger logger,
    @required ProcessManager processManager,
88 89 90 91
    @required Xcode xcode,
  })  : _logger = logger,
        _xcode = xcode,
        _processUtils = ProcessUtils(processManager: processManager, logger: logger);
92 93 94

  final Logger _logger;
  final ProcessUtils _processUtils;
95
  final Xcode _xcode;
96

97 98
  /// Runs `simctl list --json` and returns the JSON of the corresponding
  /// [section].
99
  Future<Map<String, dynamic>> _list(SimControlListSection section) async {
100 101
    // Sample output from `simctl list --json`:
    //
102
    // {
103 104
    //   "devicetypes": { ... },
    //   "runtimes": { ... },
105 106 107 108 109 110 111 112 113
    //   "devices" : {
    //     "com.apple.CoreSimulator.SimRuntime.iOS-8-2" : [
    //       {
    //         "state" : "Shutdown",
    //         "availability" : " (unavailable, runtime profile not found)",
    //         "name" : "iPhone 4s",
    //         "udid" : "1913014C-6DCB-485D-AC6B-7CD76D322F5B"
    //       },
    //       ...
114 115
    //   },
    //   "pairs": { ... },
116

117 118 119 120 121 122 123
    final List<String> command = <String>[
      ..._xcode.xcrunCommand(),
      'simctl',
      'list',
      '--json',
      section.name,
    ];
124 125
    _logger.printTrace(command.join(' '));
    final RunResult results = await _processUtils.run(command);
126
    if (results.exitCode != 0) {
127
      _logger.printError('Error executing simctl: ${results.exitCode}\n${results.stderr}');
128
      return <String, Map<String, dynamic>>{};
129
    }
130
    try {
131 132 133 134
      final Object decodeResult = json.decode(results.stdout?.toString())[section.name];
      if (decodeResult is Map<String, dynamic>) {
        return decodeResult;
      }
135
      _logger.printError('simctl returned unexpected JSON response: ${results.stdout}');
136
      return <String, dynamic>{};
137 138 139 140
    } on FormatException {
      // We failed to parse the simctl output, or it returned junk.
      // One known message is "Install Started" isn't valid JSON but is
      // returned sometimes.
141
      _logger.printError('simctl returned non-JSON response: ${results.stdout}');
142 143
      return <String, dynamic>{};
    }
144 145 146
  }

  /// Returns a list of all available devices, both potential and connected.
147
  Future<List<SimDevice>> getDevices() async {
148
    final List<SimDevice> devices = <SimDevice>[];
149

150
    final Map<String, dynamic> devicesSection = await _list(SimControlListSection.devices);
151

152
    for (final String deviceCategory in devicesSection.keys) {
153 154
      final Object devicesData = devicesSection[deviceCategory];
      if (devicesData != null && devicesData is List<dynamic>) {
155
        for (final Map<String, dynamic> data in devicesData.map<Map<String, dynamic>>(castStringKeyedMap)) {
156 157
          devices.add(SimDevice(deviceCategory, data));
        }
158 159 160 161 162 163
      }
    }

    return devices;
  }

164 165
  /// Returns all the connected simulator devices.
  Future<List<SimDevice>> getConnectedDevices() async {
166
    final List<SimDevice> simDevices = await getDevices();
167
    return simDevices.where((SimDevice device) => device.isBooted).toList();
168 169
  }

170
  Future<bool> isInstalled(String deviceId, String appId) {
171
    return _processUtils.exitsHappy(<String>[
172
      ..._xcode.xcrunCommand(),
173 174
      'simctl',
      'get_app_container',
175
      deviceId,
176 177 178 179
      appId,
    ]);
  }

180 181
  Future<RunResult> install(String deviceId, String appPath) async {
    RunResult result;
182
    try {
183
      result = await _processUtils.run(
184 185 186 187 188 189 190
        <String>[
          ..._xcode.xcrunCommand(),
          'simctl',
          'install',
          deviceId,
          appPath,
        ],
191 192
        throwOnError: true,
      );
193
    } on ProcessException catch (exception) {
194
      throwToolExit('Unable to install $appPath on $deviceId. This is sometimes caused by a malformed plist file:\n$exception');
195 196
    }
    return result;
197 198
  }

199 200
  Future<RunResult> uninstall(String deviceId, String appId) async {
    RunResult result;
201
    try {
202
      result = await _processUtils.run(
203 204 205 206 207 208 209
        <String>[
          ..._xcode.xcrunCommand(),
          'simctl',
          'uninstall',
          deviceId,
          appId,
        ],
210 211
        throwOnError: true,
      );
212 213 214 215
    } on ProcessException catch (exception) {
      throwToolExit('Unable to uninstall $appId from $deviceId:\n$exception');
    }
    return result;
216 217
  }

218 219
  Future<RunResult> launch(String deviceId, String appIdentifier, [ List<String> launchArgs ]) async {
    RunResult result;
220
    try {
221
      result = await _processUtils.run(
222
        <String>[
223
          ..._xcode.xcrunCommand(),
224 225 226 227 228 229 230 231
          'simctl',
          'launch',
          deviceId,
          appIdentifier,
          ...?launchArgs,
        ],
        throwOnError: true,
      );
232 233 234 235
    } on ProcessException catch (exception) {
      throwToolExit('Unable to launch $appIdentifier on $deviceId:\n$exception');
    }
    return result;
236
  }
237

238
  Future<void> takeScreenshot(String deviceId, String outputPath) async {
239
    try {
240
      await _processUtils.run(
241 242 243 244 245 246 247 248
        <String>[
          ..._xcode.xcrunCommand(),
          'simctl',
          'io',
          deviceId,
          'screenshot',
          outputPath,
        ],
249 250
        throwOnError: true,
      );
251
    } on ProcessException catch (exception) {
252
      _logger.printError('Unable to take screenshot of $deviceId:\n$exception');
253
    }
254
  }
255 256
}

257 258
/// Enumerates all data sections of `xcrun simctl list --json` command.
class SimControlListSection {
Ian Hickson's avatar
Ian Hickson committed
259
  const SimControlListSection._(this.name);
260 261

  final String name;
Ian Hickson's avatar
Ian Hickson committed
262

263 264 265 266
  static const SimControlListSection devices = SimControlListSection._('devices');
  static const SimControlListSection devicetypes = SimControlListSection._('devicetypes');
  static const SimControlListSection runtimes = SimControlListSection._('runtimes');
  static const SimControlListSection pairs = SimControlListSection._('pairs');
267 268
}

269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
/// A simulated device type.
///
/// Simulated device types can be listed using the command
/// `xcrun simctl list devicetypes`.
class SimDeviceType {
  SimDeviceType(this.name, this.identifier);

  /// The name of the device type.
  ///
  /// Examples:
  ///
  ///     "iPhone 6s"
  ///     "iPhone 6 Plus"
  final String name;

  /// The identifier of the device type.
  ///
  /// Examples:
  ///
  ///     "com.apple.CoreSimulator.SimDeviceType.iPhone-6s"
  ///     "com.apple.CoreSimulator.SimDeviceType.iPhone-6-Plus"
  final String identifier;
}

293 294 295 296
class SimDevice {
  SimDevice(this.category, this.data);

  final String category;
297
  final Map<String, dynamic> data;
298

299
  String get state => data['state']?.toString();
300
  String get availability => data['availability']?.toString();
301 302
  String get name => data['name']?.toString();
  String get udid => data['udid']?.toString();
303 304 305 306 307

  bool get isBooted => state == 'Booted';
}

class IOSSimulator extends Device {
308 309 310 311 312 313 314 315 316 317 318 319
  IOSSimulator(
    String id, {
      this.name,
      this.simulatorCategory,
      @required SimControl simControl,
    }) : _simControl = simControl,
         super(
           id,
           category: Category.mobile,
           platformType: PlatformType.ios,
           ephemeral: true,
         );
320

321
  @override
322 323
  final String name;

324
  final String simulatorCategory;
325

326 327
  final SimControl _simControl;

328 329 330 331
  @override
  DevFSWriter createDevFSWriter(covariant ApplicationPackage app, String userIdentifier) {
    return LocalDevFSWriter(fileSystem: globals.fs);
  }
332

333
  @override
334
  Future<bool> get isLocalEmulator async => true;
335

336 337 338
  @override
  Future<String> get emulatorId async => iosSimulatorId;

339
  @override
340 341 342 343
  bool get supportsHotReload => true;

  @override
  bool get supportsHotRestart => true;
344

345 346 347 348 349 350
  @override
  Future<bool> get supportsHardwareRendering async => false;

  @override
  bool supportsRuntimeMode(BuildMode buildMode) => buildMode == BuildMode.debug;

351
  Map<ApplicationPackage, _IOSSimulatorLogReader> _logReaders;
352
  _IOSSimulatorDevicePortForwarder _portForwarder;
353

354
  @override
355 356 357 358
  Future<bool> isAppInstalled(
    ApplicationPackage app, {
    String userIdentifier,
  }) {
359
    return _simControl.isInstalled(id, app.id);
360 361
  }

362
  @override
363
  Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => false;
364

365
  @override
366 367 368 369
  Future<bool> installApp(
    covariant IOSApp app, {
    String userIdentifier,
  }) async {
370
    try {
371
      final IOSApp iosApp = app;
372
      await _simControl.install(id, iosApp.simulatorBundlePath);
373
      return true;
374
    } on Exception {
375 376 377 378
      return false;
    }
  }

379
  @override
380 381 382 383
  Future<bool> uninstallApp(
    ApplicationPackage app, {
    String userIdentifier,
  }) async {
384
    try {
385
      await _simControl.uninstall(id, app.id);
386
      return true;
387
    } on Exception {
388 389 390 391
      return false;
    }
  }

392 393
  @override
  bool isSupported() {
394
    if (!globals.platform.isMacOS) {
395
      _supportMessage = 'iOS devices require a Mac host machine.';
396 397 398
      return false;
    }

399
    // Check if the device is part of a blocked category.
400
    // We do not yet support WatchOS or tvOS devices.
401 402
    final RegExp blocklist = RegExp(r'Apple (TV|Watch)', caseSensitive: false);
    if (blocklist.hasMatch(name)) {
403
      _supportMessage = 'Flutter does not support Apple TV or Apple Watch.';
404 405 406
      return false;
    }
    return true;
407 408
  }

409
  String _supportMessage;
410

411 412 413 414
  @override
  String supportMessage() {
    if (isSupported()) {
      return 'Supported';
415
    }
416

417
    return _supportMessage ?? 'Unknown';
418 419 420
  }

  @override
Devon Carew's avatar
Devon Carew committed
421
  Future<LaunchResult> startApp(
422
    covariant IOSApp package, {
423 424
    String mainPath,
    String route,
Devon Carew's avatar
Devon Carew committed
425
    DebuggingOptions debuggingOptions,
426
    Map<String, dynamic> platformArgs,
427 428
    bool prebuiltApplication = false,
    bool ipv6 = false,
429
    String userIdentifier,
430
  }) async {
431
    if (!prebuiltApplication && package is BuildableIOSApp) {
432
      globals.printTrace('Building ${package.name} for $id.');
433

434
      try {
435
        await _setupUpdatedApplicationBundle(package, debuggingOptions.buildInfo, mainPath);
436
      } on ToolExit catch (e) {
437
        globals.printError(e.message);
438
        return LaunchResult.failed();
439
      }
440
    } else {
441
      if (!await installApp(package)) {
442
        return LaunchResult.failed();
443
      }
444
    }
445

446
    // Prepare launch arguments.
447
    final String dartVmFlags = computeDartVmFlags(debuggingOptions);
448 449 450 451
    final List<String> args = <String>[
      '--enable-dart-profiling',
      if (debuggingOptions.debuggingEnabled) ...<String>[
        if (debuggingOptions.buildInfo.isDebug) ...<String>[
452
          '--enable-checked-mode',
453
          '--verify-entry-points',
454
        ],
455
        if (debuggingOptions.enableSoftwareRendering) '--enable-software-rendering',
456 457 458 459
        if (debuggingOptions.startPaused) '--start-paused',
        if (debuggingOptions.disableServiceAuthCodes) '--disable-service-auth-codes',
        if (debuggingOptions.skiaDeterministicRendering) '--skia-deterministic-rendering',
        if (debuggingOptions.useTestFonts) '--use-test-fonts',
460
        if (debuggingOptions.traceAllowlist != null) '--trace-allowlist="${debuggingOptions.traceAllowlist}"',
461 462
        if (dartVmFlags.isNotEmpty) '--dart-flags=$dartVmFlags',
        '--observatory-port=${debuggingOptions.hostVmServicePort ?? 0}'
463
      ],
464
    ];
465

466
    ProtocolDiscovery observatoryDiscovery;
467
    if (debuggingOptions.debuggingEnabled) {
468
      observatoryDiscovery = ProtocolDiscovery.observatory(
469 470
        getLogReader(app: package),
        ipv6: ipv6,
471
        hostPort: debuggingOptions.hostVmServicePort,
472 473
        devicePort: debuggingOptions.deviceVmServicePort,
      );
474
    }
475

476
    // Launch the updated application in the simulator.
477
    try {
478 479 480 481
      // Use the built application's Info.plist to get the bundle identifier,
      // which should always yield the correct value and does not require
      // parsing the xcodeproj or configuration files.
      // See https://github.com/flutter/flutter/issues/31037 for more information.
482
      final String plistPath = globals.fs.path.join(package.simulatorBundlePath, 'Info.plist');
483
      final String bundleIdentifier = globals.plistParser.getValueFromFile(plistPath, PlistParser.kCFBundleIdentifierKey);
484

485
      await _simControl.launch(id, bundleIdentifier, args);
486
    } on Exception catch (error) {
487
      globals.printError('$error');
488
      return LaunchResult.failed();
489 490
    }

Devon Carew's avatar
Devon Carew committed
491
    if (!debuggingOptions.debuggingEnabled) {
492
      return LaunchResult.succeeded();
493
    }
Devon Carew's avatar
Devon Carew committed
494

495 496
    // Wait for the service protocol port here. This will complete once the
    // device has printed "Observatory is listening on..."
497
    globals.printTrace('Waiting for observatory port to be available...');
498 499

    try {
500
      final Uri deviceUri = await observatoryDiscovery.uri;
501 502 503 504 505 506 507
      if (deviceUri != null) {
        return LaunchResult.succeeded(observatoryUri: deviceUri);
      }
      globals.printError(
        'Error waiting for a debug connection: '
        'The log reader failed unexpectedly',
      );
508
    } on Exception catch (error) {
509
      globals.printError('Error waiting for a debug connection: $error');
510
    } finally {
511
      await observatoryDiscovery?.cancel();
Devon Carew's avatar
Devon Carew committed
512
    }
513
    return LaunchResult.failed();
514 515
  }

516
  Future<void> _setupUpdatedApplicationBundle(covariant BuildableIOSApp app, BuildInfo buildInfo, String mainPath) async {
517
    // Step 1: Build the Xcode project.
518
    // The build mode for the simulator is always debug.
519
    assert(buildInfo.isDebug);
520

521 522
    final XcodeBuildResult buildResult = await buildXcodeProject(
      app: app,
523
      buildInfo: buildInfo,
524 525
      targetOverride: mainPath,
      buildForDevice: false,
526
      deviceID: id,
527
    );
528
    if (!buildResult.success) {
529
      throwToolExit('Could not build the application for the simulator.');
530
    }
531 532

    // Step 2: Assert that the Xcode project was successfully built.
533
    final Directory bundle = globals.fs.directory(app.simulatorBundlePath);
534
    final bool bundleExists = bundle.existsSync();
535
    if (!bundleExists) {
536
      throwToolExit('Could not find the built application bundle at ${bundle.path}.');
537
    }
538 539

    // Step 3: Install the updated bundle to the simulator.
540
    await _simControl.install(id, globals.fs.path.absolute(bundle.path));
541 542
  }

543
  @override
544 545 546 547
  Future<bool> stopApp(
    ApplicationPackage app, {
    String userIdentifier,
  }) async {
548 549 550 551 552
    // Currently we don't have a way to stop an app running on iOS.
    return false;
  }

  String get logFilePath {
553
    return globals.platform.environment.containsKey('IOS_SIMULATOR_LOG_FILE_PATH')
554 555 556 557 558 559 560 561 562
      ? globals.platform.environment['IOS_SIMULATOR_LOG_FILE_PATH'].replaceAll('%{id}', id)
      : globals.fs.path.join(
          globals.fsUtils.homeDirPath,
          'Library',
          'Logs',
          'CoreSimulator',
          id,
          'system.log',
        );
563 564 565
  }

  @override
566
  Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;
567

568
  @override
569
  Future<String> get sdkNameAndVersion async => simulatorCategory;
570

571
  final RegExp _iosSdkRegExp = RegExp(r'iOS( |-)(\d+)');
572 573 574

  Future<int> get sdkMajorVersion async {
    final Match sdkMatch = _iosSdkRegExp.firstMatch(await sdkNameAndVersion);
575
    return int.parse(sdkMatch?.group(2) ?? '11');
576 577
  }

578
  @override
579 580 581 582
  DeviceLogReader getLogReader({
    covariant IOSApp app,
    bool includePastLogs = false,
  }) {
583
    assert(app == null || app is IOSApp);
584
    assert(!includePastLogs, 'Past log reading not supported on iOS simulators.');
585
    _logReaders ??= <ApplicationPackage, _IOSSimulatorLogReader>{};
586
    return _logReaders.putIfAbsent(app, () => _IOSSimulatorLogReader(this, app));
587
  }
588

589
  @override
590
  DevicePortForwarder get portForwarder => _portForwarder ??= _IOSSimulatorDevicePortForwarder(this);
591

592
  @override
593
  void clearLogs() {
594
    final File logFile = globals.fs.file(logFilePath);
595
    if (logFile.existsSync()) {
596
      final RandomAccessFile randomFile = logFile.openSync(mode: FileMode.write);
597 598 599 600 601
      randomFile.truncateSync(0);
      randomFile.closeSync();
    }
  }

602
  Future<void> ensureLogsExists() async {
603
    if (await sdkMajorVersion < 11) {
604
      final File logFile = globals.fs.file(logFilePath);
605
      if (!logFile.existsSync()) {
606
        logFile.writeAsBytesSync(<int>[]);
607
      }
608
    }
609
  }
Devon Carew's avatar
Devon Carew committed
610 611

  @override
612
  bool get supportsScreenshot => true;
Devon Carew's avatar
Devon Carew committed
613

614
  @override
615
  Future<void> takeScreenshot(File outputFile) {
616
    return _simControl.takeScreenshot(id, outputFile.path);
Devon Carew's avatar
Devon Carew committed
617
  }
618 619 620 621 622

  @override
  bool isSupportedForProject(FlutterProject flutterProject) {
    return flutterProject.ios.existsSync();
  }
623 624 625 626 627 628 629 630 631 632

  @override
  Future<void> dispose() async {
    _logReaders?.forEach(
      (ApplicationPackage application, _IOSSimulatorLogReader logReader) {
        logReader.dispose();
      },
    );
    await _portForwarder?.dispose();
  }
633 634
}

635 636 637
/// Launches the device log reader process on the host and parses the syslog.
@visibleForTesting
Future<Process> launchDeviceSystemLogTool(IOSSimulator device) async {
638
  return globals.processUtils.start(<String>['tail', '-n', '0', '-F', device.logFilePath]);
639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662
}

/// Launches the device log reader process on the host and parses unified logging.
@visibleForTesting
Future<Process> launchDeviceUnifiedLogging (IOSSimulator device, String appName) async {
  // Make NSPredicate concatenation easier to read.
  String orP(List<String> clauses) => '(${clauses.join(" OR ")})';
  String andP(List<String> clauses) => clauses.join(' AND ');
  String notP(String clause) => 'NOT($clause)';

  final String predicate = andP(<String>[
    'eventType = logEvent',
    if (appName != null) 'processImagePath ENDSWITH "$appName"',
    // Either from Flutter or Swift (maybe assertion or fatal error) or from the app itself.
    orP(<String>[
      'senderImagePath ENDSWITH "/Flutter"',
      'senderImagePath ENDSWITH "/libswiftCore.dylib"',
      'processImageUUID == senderImageUUID',
    ]),
    // Filter out some messages that clearly aren't related to Flutter.
    notP('eventMessage CONTAINS ": could not find icon for representation -> com.apple."'),
    notP('eventMessage BEGINSWITH "assertion failed: "'),
    notP('eventMessage CONTAINS " libxpc.dylib "'),
  ]);
663

664
  return globals.processUtils.start(<String>[
665 666 667 668 669 670 671 672 673 674
    ...globals.xcode.xcrunCommand(),
    'simctl',
    'spawn',
    device.id,
    'log',
    'stream',
    '--style',
    'json',
    '--predicate',
    predicate,
675 676 677
  ]);
}

678
@visibleForTesting
679 680
Future<Process> launchSystemLogTool(IOSSimulator device) async {
  // Versions of iOS prior to 11 tail the simulator syslog file.
681
  if (await device.sdkMajorVersion < 11) {
682
    return globals.processUtils.start(<String>['tail', '-n', '0', '-F', '/private/var/log/system.log']);
683
  }
684 685 686 687 688

  // For iOS 11 and later, all relevant detail is in the device log.
  return null;
}

689
class _IOSSimulatorLogReader extends DeviceLogReader {
690
  _IOSSimulatorLogReader(this.device, IOSApp app) {
691
    _linesController = StreamController<String>.broadcast(
692
      onListen: _start,
693
      onCancel: _stop,
Devon Carew's avatar
Devon Carew committed
694
    );
695
    _appName = app == null ? null : app.name.replaceAll('.app', '');
Devon Carew's avatar
Devon Carew committed
696
  }
697 698 699

  final IOSSimulator device;

700 701
  String _appName;

Devon Carew's avatar
Devon Carew committed
702
  StreamController<String> _linesController;
703

Devon Carew's avatar
Devon Carew committed
704
  // We log from two files: the device and the system log.
705 706
  Process _deviceProcess;
  Process _systemProcess;
707

708
  @override
Devon Carew's avatar
Devon Carew committed
709
  Stream<String> get logLines => _linesController.stream;
710

711
  @override
712 713
  String get name => device.name;

714
  Future<void> _start() async {
715 716 717 718 719 720 721 722 723 724 725 726
    // Unified logging iOS 11 and greater (introduced in iOS 10).
    if (await device.sdkMajorVersion >= 11) {
      _deviceProcess = await launchDeviceUnifiedLogging(device, _appName);
      _deviceProcess.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onUnifiedLoggingLine);
      _deviceProcess.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onUnifiedLoggingLine);
    } else {
      // Fall back to syslog parsing.
      await device.ensureLogsExists();
      _deviceProcess = await launchDeviceSystemLogTool(device);
      _deviceProcess.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onSysLogDeviceLine);
      _deviceProcess.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onSysLogDeviceLine);
    }
727 728 729

    // Track system.log crashes.
    // ReportCrash[37965]: Saved crash report for FlutterRunner[37941]...
730 731
    _systemProcess = await launchSystemLogTool(device);
    if (_systemProcess != null) {
732 733
      _systemProcess.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onSystemLine);
      _systemProcess.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onSystemLine);
734
    }
735

736 737
    // We don't want to wait for the process or its callback. Best effort
    // cleanup in the callback.
738
    unawaited(_deviceProcess.exitCode.whenComplete(() {
739
      if (_linesController.hasListener) {
Devon Carew's avatar
Devon Carew committed
740
        _linesController.close();
741
      }
742
    }));
743 744 745
  }

  // Match the log prefix (in order to shorten it):
746 747
  // * Xcode 8: Sep 13 15:28:51 cbracken-macpro localhost Runner[37195]: (Flutter) Observatory listening on http://127.0.0.1:57701/
  // * Xcode 9: 2017-09-13 15:26:57.228948-0700  localhost Runner[37195]: (Flutter) Observatory listening on http://127.0.0.1:57701/
748
  static final RegExp _mapRegex = RegExp(r'\S+ +\S+ +(?:\S+) (.+?(?=\[))\[\d+\]\)?: (\(.*?\))? *(.*)$');
749 750

  // Jan 31 19:23:28 --- last message repeated 1 time ---
751 752
  static final RegExp _lastMessageSingleRegex = RegExp(r'\S+ +\S+ +\S+ --- last message repeated 1 time ---$');
  static final RegExp _lastMessageMultipleRegex = RegExp(r'\S+ +\S+ +\S+ --- last message repeated (\d+) times ---$');
753

754
  static final RegExp _flutterRunnerRegex = RegExp(r' FlutterRunner\[\d+\] ');
755

756 757 758 759
  // Remember what we did with the last line, in case we need to process
  // a multiline record
  bool _lastLineMatched = false;

760
  String _filterDeviceLine(String string) {
761
    final Match match = _mapRegex.matchAsPrefix(string);
762
    if (match != null) {
763

764 765 766 767 768 769 770 771 772
      // The category contains the text between the date and the PID. Depending on which version of iOS being run,
      // it can contain "hostname App Name" or just "App Name".
      final String category = match.group(1);
      final String tag = match.group(2);
      final String content = match.group(3);

      // Filter out log lines from an app other than this one (category doesn't match the app name).
      // If the hostname is included in the category, check that it doesn't end with the app name.
      if (_appName != null && !category.endsWith(_appName)) {
773
        return null;
774
      }
775

776
      if (tag != null && tag != '(Flutter)') {
777
        return null;
778
      }
779

780
      // Filter out some messages that clearly aren't related to Flutter.
781
      if (string.contains(': could not find icon for representation -> com.apple.')) {
782
        return null;
783
      }
784

785
      // assertion failed: 15G1212 13E230: libxpc.dylib + 57882 [66C28065-C9DB-3C8E-926F-5A40210A6D1B]: 0x7d
786
      if (content.startsWith('assertion failed: ') && content.contains(' libxpc.dylib ')) {
787
        return null;
788
      }
789

790
      if (_appName == null) {
791
        return '$category: $content';
792
      } else if (category == _appName || category.endsWith(' $_appName')) {
793
        return content;
794
      }
795 796

      return null;
797
    }
798

799
    if (string.startsWith('Filtering the log data using ')) {
800
      return null;
801
    }
802

803
    if (string.startsWith('Timestamp                       (process)[PID]')) {
804
      return null;
805
    }
806

807
    if (_lastMessageSingleRegex.matchAsPrefix(string) != null) {
808
      return null;
809
    }
810

811
    if (RegExp(r'assertion failed: .* libxpc.dylib .* 0x7d$').matchAsPrefix(string) != null) {
812
      return null;
813
    }
814

815 816 817 818 819
    // Starts with space(s) - continuation of the multiline message
    if (RegExp(r'\s+').matchAsPrefix(string) != null && !_lastLineMatched) {
      return null;
    }

820 821 822
    return string;
  }

823 824
  String _lastLine;

825
  void _onSysLogDeviceLine(String line) {
826
    globals.printTrace('[DEVICE LOG] $line');
827
    final Match multi = _lastMessageMultipleRegex.matchAsPrefix(line);
828 829 830 831 832

    if (multi != null) {
      if (_lastLine != null) {
        int repeat = int.parse(multi.group(1));
        repeat = math.max(0, math.min(100, repeat));
833
        for (int i = 1; i < repeat; i++) {
834
          _linesController.add(_lastLine);
835
        }
836 837 838
      }
    } else {
      _lastLine = _filterDeviceLine(line);
839
      if (_lastLine != null) {
840
        _linesController.add(_lastLine);
841 842 843
        _lastLineMatched = true;
      } else {
        _lastLineMatched = false;
844
      }
845
    }
846 847
  }

848 849 850 851 852 853 854 855 856 857 858 859 860
  //   "eventMessage" : "flutter: 21",
  static final RegExp _unifiedLoggingEventMessageRegex = RegExp(r'.*"eventMessage" : (".*")');
  void _onUnifiedLoggingLine(String line) {
    // The log command predicate handles filtering, so every log eventMessage should be decoded and added.
    final Match eventMessageMatch = _unifiedLoggingEventMessageRegex.firstMatch(line);
    if (eventMessageMatch != null) {
      final dynamic decodedJson = jsonDecode(eventMessageMatch.group(1));
      if (decodedJson is String) {
        _linesController.add(decodedJson);
      }
    }
  }

861
  String _filterSystemLog(String string) {
862
    final Match match = _mapRegex.matchAsPrefix(string);
863 864 865 866
    return match == null ? string : '${match.group(1)}: ${match.group(2)}';
  }

  void _onSystemLine(String line) {
867
    globals.printTrace('[SYS LOG] $line');
868
    if (!_flutterRunnerRegex.hasMatch(line)) {
869
      return;
870
    }
871

872
    final String filteredLine = _filterSystemLog(line);
873
    if (filteredLine == null) {
874
      return;
875
    }
876

Devon Carew's avatar
Devon Carew committed
877
    _linesController.add(filteredLine);
878 879
  }

Devon Carew's avatar
Devon Carew committed
880 881 882
  void _stop() {
    _deviceProcess?.kill();
    _systemProcess?.kill();
883
  }
884 885 886 887 888

  @override
  void dispose() {
    _stop();
  }
889
}
890 891

int compareIosVersions(String v1, String v2) {
892 893
  final List<int> v1Fragments = v1.split('.').map<int>(int.parse).toList();
  final List<int> v2Fragments = v2.split('.').map<int>(int.parse).toList();
894 895

  int i = 0;
896
  while (i < v1Fragments.length && i < v2Fragments.length) {
897 898
    final int v1Fragment = v1Fragments[i];
    final int v2Fragment = v2Fragments[i];
899
    if (v1Fragment != v2Fragment) {
900
      return v1Fragment.compareTo(v2Fragment);
901
    }
902
    i += 1;
903 904 905 906 907 908 909
  }
  return v1Fragments.length.compareTo(v2Fragments.length);
}

/// Matches on device type given an identifier.
///
/// Example device type identifiers:
910 911 912 913 914 915
///
/// - ✓ com.apple.CoreSimulator.SimDeviceType.iPhone-5
/// - ✓ com.apple.CoreSimulator.SimDeviceType.iPhone-6
/// - ✓ com.apple.CoreSimulator.SimDeviceType.iPhone-6s-Plus
/// - ✗ com.apple.CoreSimulator.SimDeviceType.iPad-2
/// - ✗ com.apple.CoreSimulator.SimDeviceType.Apple-Watch-38mm
916
final RegExp _iosDeviceTypePattern =
917
    RegExp(r'com.apple.CoreSimulator.SimDeviceType.iPhone-(\d+)(.*)');
918 919

int compareIphoneVersions(String id1, String id2) {
920 921
  final Match m1 = _iosDeviceTypePattern.firstMatch(id1);
  final Match m2 = _iosDeviceTypePattern.firstMatch(id2);
922

923 924
  final int v1 = int.parse(m1[1]);
  final int v2 = int.parse(m2[1]);
925

926
  if (v1 != v2) {
927
    return v1.compareTo(v2);
928
  }
929 930

  // Sorted in the least preferred first order.
931
  const List<String> qualifiers = <String>['-Plus', '', 's-Plus', 's'];
932

933 934
  final int q1 = qualifiers.indexOf(m1[2]);
  final int q2 = qualifiers.indexOf(m2[2]);
935 936
  return q1.compareTo(q2);
}
937 938 939 940 941 942 943 944

class _IOSSimulatorDevicePortForwarder extends DevicePortForwarder {
  _IOSSimulatorDevicePortForwarder(this.device);

  final IOSSimulator device;

  final List<ForwardedPort> _ports = <ForwardedPort>[];

945
  @override
946
  List<ForwardedPort> get forwardedPorts => _ports;
947

948
  @override
949
  Future<int> forward(int devicePort, { int hostPort }) async {
950
    if (hostPort == null || hostPort == 0) {
951 952 953
      hostPort = devicePort;
    }
    assert(devicePort == hostPort);
954
    _ports.add(ForwardedPort(devicePort, hostPort));
955 956 957
    return hostPort;
  }

958
  @override
959
  Future<void> unforward(ForwardedPort forwardedPort) async {
960 961
    _ports.remove(forwardedPort);
  }
962 963 964

  @override
  Future<void> dispose() async {
965
    final List<ForwardedPort> portsCopy = List<ForwardedPort>.of(_ports);
966
    for (final ForwardedPort port in portsCopy) {
967 968 969
      await unforward(port);
    }
  }
970
}