add the ability to start and stop device polling
diff --git a/packages/flutter_tools/lib/src/ios/device_ios.dart b/packages/flutter_tools/lib/src/ios/device_ios.dart deleted file mode 100644 index 10bb965..0000000 --- a/packages/flutter_tools/lib/src/ios/device_ios.dart +++ /dev/null
@@ -1,712 +0,0 @@ -// Copyright 2016 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:path/path.dart' as path; - -import '../application_package.dart'; -import '../artifacts.dart'; -import '../base/common.dart'; -import '../base/process.dart'; -import '../build_configuration.dart'; -import '../device.dart'; -import '../globals.dart'; -import '../ios/setup_xcodeproj.dart'; -import '../services.dart'; -import '../toolchain.dart'; -import 'simulator.dart'; - -const String _ideviceinstallerInstructions = - 'To work with iOS devices, please install ideviceinstaller.\n' - 'If you use homebrew, you can install it with "\$ brew install ideviceinstaller".'; - -class IOSDeviceDiscovery extends DeviceDiscovery { - List<Device> _devices = <Device>[]; - - bool get supportsPlatform => Platform.isMacOS; - - Future init() { - _devices = IOSDevice.getAttachedDevices(); - return new Future.value(); - } - - List<Device> get devices => _devices; -} - -class IOSSimulatorDiscovery extends DeviceDiscovery { - List<Device> _devices = <Device>[]; - - bool get supportsPlatform => Platform.isMacOS; - - Future init() { - _devices = IOSSimulator.getAttachedDevices(); - return new Future.value(); - } - - List<Device> get devices => _devices; -} - -class IOSDevice extends Device { - IOSDevice(String id, { this.name }) : super(id) { - _installerPath = _checkForCommand('ideviceinstaller'); - _listerPath = _checkForCommand('idevice_id'); - _informerPath = _checkForCommand('ideviceinfo'); - _debuggerPath = _checkForCommand('idevicedebug'); - _loggerPath = _checkForCommand('idevicesyslog'); - _pusherPath = _checkForCommand( - 'ios-deploy', - 'To copy files to iOS devices, please install ios-deploy. ' - 'You can do this using homebrew as follows:\n' - '\$ brew tap flutter/flutter\n' - '\$ brew install ios-deploy'); - } - - String _installerPath; - String get installerPath => _installerPath; - - String _listerPath; - String get listerPath => _listerPath; - - String _informerPath; - String get informerPath => _informerPath; - - String _debuggerPath; - String get debuggerPath => _debuggerPath; - - String _loggerPath; - String get loggerPath => _loggerPath; - - String _pusherPath; - String get pusherPath => _pusherPath; - - final String name; - - bool get supportsStartPaused => false; - - static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) { - if (!doctor.iosWorkflow.hasIdeviceId) - return <IOSDevice>[]; - - List<IOSDevice> devices = []; - for (String id in _getAttachedDeviceIDs(mockIOS)) { - String name = _getDeviceName(id, mockIOS); - devices.add(new IOSDevice(id, name: name)); - } - return devices; - } - - static Iterable<String> _getAttachedDeviceIDs([IOSDevice mockIOS]) { - String listerPath = (mockIOS != null) ? mockIOS.listerPath : _checkForCommand('idevice_id'); - try { - String output = runSync([listerPath, '-l']); - return output.trim().split('\n').where((String s) => s != null && s.isNotEmpty); - } catch (e) { - return <String>[]; - } - } - - static String _getDeviceName(String deviceID, [IOSDevice mockIOS]) { - String informerPath = (mockIOS != null) - ? mockIOS.informerPath - : _checkForCommand('ideviceinfo'); - return runSync([informerPath, '-k', 'DeviceName', '-u', deviceID]).trim(); - } - - static final Map<String, String> _commandMap = {}; - static String _checkForCommand( - String command, [ - String macInstructions = _ideviceinstallerInstructions - ]) { - return _commandMap.putIfAbsent(command, () { - try { - command = runCheckedSync(['which', command]).trim(); - } catch (e) { - if (Platform.isMacOS) { - printError('$command not found. $macInstructions'); - } else { - printError('Cannot control iOS devices or simulators. $command is not available on your platform.'); - } - } - return command; - }); - } - - @override - bool installApp(ApplicationPackage app) { - try { - runCheckedSync([installerPath, '-i', app.localPath]); - return true; - } catch (e) { - return false; - } - return false; - } - - @override - bool isConnected() => _getAttachedDeviceIDs().contains(id); - - @override - bool isSupported() => true; - - @override - bool isAppInstalled(ApplicationPackage app) { - try { - String apps = runCheckedSync([installerPath, '--list-apps']); - if (new RegExp(app.id, multiLine: true).hasMatch(apps)) { - return true; - } - } catch (e) { - return false; - } - return false; - } - - @override - Future<bool> startApp( - ApplicationPackage app, - Toolchain toolchain, { - String mainPath, - String route, - bool checked: true, - bool clearLogs: false, - bool startPaused: false, - int debugPort: observatoryDefaultPort, - Map<String, dynamic> platformArgs - }) async { - // TODO(chinmaygarde): Use checked, mainPath, route, clearLogs. - // TODO(devoncarew): Handle startPaused, debugPort. - printTrace('Building ${app.name} for $id'); - - // Step 1: Install the precompiled application if necessary. - bool buildResult = await _buildIOSXcodeProject(app, buildForDevice: true); - if (!buildResult) { - printError('Could not build the precompiled application for the device.'); - return false; - } - - // Step 2: Check that the application exists at the specified path. - Directory bundle = new Directory(path.join(app.localPath, 'build', 'Release-iphoneos', 'Runner.app')); - bool bundleExists = bundle.existsSync(); - if (!bundleExists) { - printError('Could not find the built application bundle at ${bundle.path}.'); - return false; - } - - // Step 3: Attempt to install the application on the device. - int installationResult = await runCommandAndStreamOutput([ - '/usr/bin/env', - 'ios-deploy', - '--id', - id, - '--bundle', - bundle.path, - ]); - - if (installationResult != 0) { - printError('Could not install ${bundle.path} on $id.'); - return false; - } - - printTrace('Installation successful.'); - return true; - } - - @override - Future<bool> stopApp(ApplicationPackage app) async { - // Currently we don't have a way to stop an app running on iOS. - return false; - } - - Future<bool> pushFile(ApplicationPackage app, String localFile, String targetFile) async { - if (Platform.isMacOS) { - runSync(<String>[ - pusherPath, - '-t', - '1', - '--bundle_id', - app.id, - '--upload', - localFile, - '--to', - targetFile - ]); - return true; - } else { - return false; - } - return false; - } - - @override - TargetPlatform get platform => TargetPlatform.iOS; - - DeviceLogReader createLogReader() => new _IOSDeviceLogReader(this); -} - -class IOSSimulator extends Device { - IOSSimulator(String id, { this.name }) : super(id); - - static List<IOSSimulator> getAttachedDevices() { - if (!xcode.isInstalledAndMeetsVersionCheck) - return <IOSSimulator>[]; - - return SimControl.getConnectedDevices().map((SimDevice device) { - return new IOSSimulator(device.udid, name: device.name); - }).toList(); - } - - final String name; - - String get xcrunPath => path.join('/usr', 'bin', 'xcrun'); - - String _getSimulatorPath() { - return path.join(_homeDirectory, 'Library', 'Developer', 'CoreSimulator', 'Devices', id); - } - - String _getSimulatorAppHomeDirectory(ApplicationPackage app) { - String simulatorPath = _getSimulatorPath(); - if (simulatorPath == null) - return null; - return path.join(simulatorPath, 'data'); - } - - @override - bool installApp(ApplicationPackage app) { - if (!isConnected()) - return false; - - try { - SimControl.install(id, app.localPath); - return true; - } catch (e) { - return false; - } - } - - @override - bool isConnected() { - if (!Platform.isMacOS) - return false; - return SimControl.getConnectedDevices().any((SimDevice device) => device.udid == id); - } - - @override - bool isSupported() { - if (!Platform.isMacOS) { - _supportMessage = "Not supported on a non Mac host"; - return false; - } - - // Step 1: Check if the device is part of a blacklisted category. - // We do not support WatchOS or tvOS devices. - - RegExp blacklist = new RegExp(r'Apple (TV|Watch)', caseSensitive: false); - - if (blacklist.hasMatch(name)) { - _supportMessage = "Flutter does not support either the Apple TV or Watch. Choose an iPhone 5s or above."; - return false; - } - - // Step 2: Check if the device must be rejected because of its version. - // There is an artitifical check on older simulators where arm64 - // targetted applications cannot be run (even though the - // Flutter runner on the simulator is completely different). - - RegExp versionExp = new RegExp(r'iPhone ([0-9])+'); - Match match = versionExp.firstMatch(name); - - if (match == null) { - // Not an iPhone. All available non-iPhone simulators are compatible. - return true; - } - - if (int.parse(match.group(1)) > 5) { - // iPhones 6 and above are always fine. - return true; - } - - // The 's' subtype of 5 is compatible. - if (name.contains('iPhone 5s')) { - return true; - } - - _supportMessage = "The simulator version is too old. Choose an iPhone 5s or above."; - return false; - } - - String _supportMessage; - - @override - String supportMessage() { - if (isSupported()) { - return "Supported"; - } - - return _supportMessage != null ? _supportMessage : "Unknown"; - } - - @override - bool isAppInstalled(ApplicationPackage app) { - try { - String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app); - return FileSystemEntity.isDirectorySync(simulatorHomeDirectory); - } catch (e) { - return false; - } - } - - @override - Future<bool> startApp( - ApplicationPackage app, - Toolchain toolchain, { - String mainPath, - String route, - bool checked: true, - bool clearLogs: false, - bool startPaused: false, - int debugPort: observatoryDefaultPort, - Map<String, dynamic> platformArgs - }) async { - // TODO(chinmaygarde): Use mainPath, route. - printTrace('Building ${app.name} for $id.'); - - if (clearLogs) - this.clearLogs(); - - // Step 1: Build the Xcode project. - bool buildResult = await _buildIOSXcodeProject(app, buildForDevice: false); - if (!buildResult) { - printError('Could not build the application for the simulator.'); - return false; - } - - // Step 2: Assert that the Xcode project was successfully built. - Directory bundle = new Directory(path.join(app.localPath, 'build', 'Release-iphonesimulator', 'Runner.app')); - bool bundleExists = await bundle.exists(); - if (!bundleExists) { - printError('Could not find the built application bundle at ${bundle.path}.'); - return false; - } - - // Step 3: Install the updated bundle to the simulator. - SimControl.install(id, path.absolute(bundle.path)); - - // Step 4: Prepare launch arguments. - List<String> args = <String>[]; - - if (checked) - args.add("--enable-checked-mode"); - - if (startPaused) - args.add("--start-paused"); - - if (debugPort != observatoryDefaultPort) - args.add("--observatory-port=$debugPort"); - - // Step 5: Launch the updated application in the simulator. - try { - SimControl.launch(id, app.id, args); - } catch (error) { - printError('$error'); - return false; - } - - printTrace('Successfully started ${app.name} on $id.'); - - return true; - } - - @override - Future<bool> stopApp(ApplicationPackage app) async { - // Currently we don't have a way to stop an app running on iOS. - return false; - } - - Future<bool> pushFile( - ApplicationPackage app, String localFile, String targetFile) async { - if (Platform.isMacOS) { - String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app); - runCheckedSync(<String>['cp', localFile, path.join(simulatorHomeDirectory, targetFile)]); - return true; - } - return false; - } - - String get logFilePath { - return path.join(_homeDirectory, 'Library', 'Logs', 'CoreSimulator', id, 'system.log'); - } - - @override - TargetPlatform get platform => TargetPlatform.iOSSimulator; - - DeviceLogReader createLogReader() => new _IOSSimulatorLogReader(this); - - void clearLogs() { - File logFile = new File(logFilePath); - if (logFile.existsSync()) { - RandomAccessFile randomFile = logFile.openSync(mode: FileMode.WRITE); - randomFile.truncateSync(0); - randomFile.closeSync(); - } - } - - void ensureLogsExists() { - File logFile = new File(logFilePath); - if (!logFile.existsSync()) - logFile.writeAsBytesSync(<int>[]); - } -} - -class _IOSDeviceLogReader extends DeviceLogReader { - _IOSDeviceLogReader(this.device); - - final IOSDevice device; - - String get name => device.name; - - // TODO(devoncarew): Support [clear]. - Future<int> logs({ bool clear: false }) async { - if (!device.isConnected()) - return 2; - - return await runCommandAndStreamOutput( - <String>[device.loggerPath], - prefix: '[$name] ', - filter: new RegExp(r'Runner') - ); - } - - int get hashCode => name.hashCode; - - bool operator ==(dynamic other) { - if (identical(this, other)) - return true; - if (other is! _IOSDeviceLogReader) - return false; - return other.name == name; - } -} - -class _IOSSimulatorLogReader extends DeviceLogReader { - _IOSSimulatorLogReader(this.device); - - final IOSSimulator device; - - bool _lastWasFiltered = false; - - String get name => device.name; - - Future<int> logs({ bool clear: false }) async { - if (!device.isConnected()) - return 2; - - if (clear) - device.clearLogs(); - - device.ensureLogsExists(); - - // Match the log prefix (in order to shorten it): - // 'Jan 29 01:31:44 devoncarew-macbookpro3 SpringBoard[96648]: ...' - RegExp mapRegex = new RegExp(r'\S+ +\S+ +\S+ \S+ (.+)\[\d+\]\)?: (.*)$'); - // Jan 31 19:23:28 --- last message repeated 1 time --- - RegExp lastMessageRegex = new RegExp(r'\S+ +\S+ +\S+ --- (.*) ---$'); - - // This filter matches many Flutter lines in the log: - // new RegExp(r'(FlutterRunner|flutter.runner.Runner|$id)'), but it misses - // a fair number, including ones that would be useful in diagnosing crashes. - // For now, we're not filtering the log file (but do clear it with each run). - - Future<int> result = runCommandAndStreamOutput( - <String>['tail', '-n', '+0', '-F', device.logFilePath], - prefix: '[$name] ', - mapFunction: (String string) { - Match match = mapRegex.matchAsPrefix(string); - if (match != null) { - _lastWasFiltered = true; - - // Filter out some messages that clearly aren't related to Flutter. - if (string.contains(': could not find icon for representation -> com.apple.')) - return null; - String category = match.group(1); - String content = match.group(2); - if (category == 'Game Center' || category == 'itunesstored' || category == 'nanoregistrylaunchd' || - category == 'mstreamd' || category == 'syncdefaultsd' || category == 'companionappd' || - category == 'searchd') - return null; - - _lastWasFiltered = false; - - if (category == 'Runner') - return content; - return '$category: $content'; - } - match = lastMessageRegex.matchAsPrefix(string); - if (match != null && !_lastWasFiltered) - return '(${match.group(1)})'; - return string; - } - ); - - // Track system.log crashes. - // ReportCrash[37965]: Saved crash report for FlutterRunner[37941]... - runCommandAndStreamOutput( - <String>['tail', '-F', '/private/var/log/system.log'], - prefix: '[$name] ', - filter: new RegExp(r' FlutterRunner\[\d+\] '), - mapFunction: (String string) { - Match match = mapRegex.matchAsPrefix(string); - return match == null ? string : '${match.group(1)}: ${match.group(2)}'; - } - ); - - return await result; - } - - int get hashCode => device.logFilePath.hashCode; - - bool operator ==(dynamic other) { - if (identical(this, other)) - return true; - if (other is! _IOSSimulatorLogReader) - return false; - return other.device.logFilePath == device.logFilePath; - } -} - -final RegExp _xcodeVersionRegExp = new RegExp(r'Xcode (\d+)\..*'); -final String _xcodeRequirement = 'Xcode 7.0 or greater is required to develop for iOS.'; - -String get _homeDirectory => path.absolute(Platform.environment['HOME']); - -bool _checkXcodeVersion() { - if (!Platform.isMacOS) - return false; - try { - String version = runCheckedSync(<String>['xcodebuild', '-version']); - Match match = _xcodeVersionRegExp.firstMatch(version); - if (int.parse(match[1]) < 7) { - printError('Found "${match[0]}". $_xcodeRequirement'); - return false; - } - } catch (e) { - printError('Cannot find "xcodebuid". $_xcodeRequirement'); - return false; - } - return true; -} - -bool _validateEngineRevision(ApplicationPackage app) { - String skyRevision = ArtifactStore.engineRevision; - String iosRevision = _getIOSEngineRevision(app); - - if (iosRevision != skyRevision) { - printError("Error: incompatible sky_engine revision."); - printStatus('sky_engine revision: $skyRevision, iOS engine revision: $iosRevision'); - return false; - } else { - printTrace('sky_engine revision: $skyRevision, iOS engine revision: $iosRevision'); - return true; - } -} - -String _getIOSEngineRevision(ApplicationPackage app) { - File revisionFile = new File(path.join(app.localPath, 'REVISION')); - if (revisionFile.existsSync()) { - return revisionFile.readAsStringSync().trim(); - } else { - return null; - } -} - -Future<bool> _buildIOSXcodeProject(ApplicationPackage app, { bool buildForDevice }) async { - String flutterProjectPath = Directory.current.path; - - if (xcodeProjectRequiresUpdate()) { - printTrace('Initializing the Xcode project.'); - if ((await setupXcodeProjectHarness(flutterProjectPath)) != 0) { - printError('Could not initialize the Xcode project.'); - return false; - } - } else { - updateXcodeLocalProperties(flutterProjectPath); - } - - if (!_validateEngineRevision(app)) - return false; - - if (!_checkXcodeVersion()) - return false; - - // Before the build, all service definitions must be updated and the dylibs - // copied over to a location that is suitable for Xcodebuild to find them. - - await _addServicesToBundle(new Directory(app.localPath)); - - List<String> commands = <String>[ - '/usr/bin/env', 'xcrun', 'xcodebuild', '-target', 'Runner', '-configuration', 'Release' - ]; - - if (buildForDevice) { - commands.addAll(<String>['-sdk', 'iphoneos', '-arch', 'arm64']); - } else { - commands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']); - } - - try { - runCheckedSync(commands, workingDirectory: app.localPath); - return true; - } catch (error) { - return false; - } -} - -Future _addServicesToBundle(Directory bundle) async { - List<Map<String, String>> services = []; - printTrace("Trying to resolve native pub services."); - - // Step 1: Parse the service configuration yaml files present in the service - // pub packages. - await parseServiceConfigs(services); - printTrace("Found ${services.length} service definition(s)."); - - // Step 2: Copy framework dylibs to the correct spot for xcodebuild to pick up. - Directory frameworksDirectory = new Directory(path.join(bundle.path, "Frameworks")); - await _copyServiceFrameworks(services, frameworksDirectory); - - // Step 3: Copy the service definitions manifest at the correct spot for - // xcodebuild to pick up. - File manifestFile = new File(path.join(bundle.path, "ServiceDefinitions.json")); - _copyServiceDefinitionsManifest(services, manifestFile); -} - -Future _copyServiceFrameworks(List<Map<String, String>> services, Directory frameworksDirectory) async { - printTrace("Copying service frameworks to '${path.absolute(frameworksDirectory.path)}'."); - frameworksDirectory.createSync(recursive: true); - for (Map<String, String> service in services) { - String dylibPath = await getServiceFromUrl(service['ios-framework'], service['root'], service['name']); - File dylib = new File(dylibPath); - printTrace("Copying ${dylib.path} into bundle."); - if (!dylib.existsSync()) { - printError("The service dylib '${dylib.path}' does not exist."); - continue; - } - // Shell out so permissions on the dylib are preserved. - runCheckedSync(['/bin/cp', dylib.path, frameworksDirectory.path]); - } -} - -void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File manifest) { - printTrace("Creating service definitions manifest at '${manifest.path}'"); - List<Map<String, String>> jsonServices = services.map((Map<String, String> service) => { - 'name': service['name'], - // Since we have already moved it to the Frameworks directory. Strip away - // the directory and basenames. - 'framework': path.basenameWithoutExtension(service['ios-framework']) - }).toList(); - Map<String, dynamic> json = { 'services' : jsonServices }; - manifest.writeAsStringSync(JSON.encode(json), mode: FileMode.WRITE, flush: true); -}
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart new file mode 100644 index 0000000..b5900f0 --- /dev/null +++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -0,0 +1,255 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import '../application_package.dart'; +import '../base/common.dart'; +import '../base/process.dart'; +import '../build_configuration.dart'; +import '../device.dart'; +import '../globals.dart'; +import '../toolchain.dart'; +import 'mac.dart'; + +const String _ideviceinstallerInstructions = + 'To work with iOS devices, please install ideviceinstaller.\n' + 'If you use homebrew, you can install it with "\$ brew install ideviceinstaller".'; + +class IOSDevices extends PollingDeviceDiscovery { + IOSDevices() : super('IOSDevices'); + + bool get supportsPlatform => Platform.isMacOS; + List<Device> pollingGetDevices() => IOSDevice.getAttachedDevices(); +} + +class IOSDevice extends Device { + IOSDevice(String id, { this.name }) : super(id) { + _installerPath = _checkForCommand('ideviceinstaller'); + _listerPath = _checkForCommand('idevice_id'); + _informerPath = _checkForCommand('ideviceinfo'); + _debuggerPath = _checkForCommand('idevicedebug'); + _loggerPath = _checkForCommand('idevicesyslog'); + _pusherPath = _checkForCommand( + 'ios-deploy', + 'To copy files to iOS devices, please install ios-deploy. ' + 'You can do this using homebrew as follows:\n' + '\$ brew tap flutter/flutter\n' + '\$ brew install ios-deploy'); + } + + String _installerPath; + String get installerPath => _installerPath; + + String _listerPath; + String get listerPath => _listerPath; + + String _informerPath; + String get informerPath => _informerPath; + + String _debuggerPath; + String get debuggerPath => _debuggerPath; + + String _loggerPath; + String get loggerPath => _loggerPath; + + String _pusherPath; + String get pusherPath => _pusherPath; + + final String name; + + bool get supportsStartPaused => false; + + static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) { + if (!doctor.iosWorkflow.hasIdeviceId) + return <IOSDevice>[]; + + List<IOSDevice> devices = []; + for (String id in _getAttachedDeviceIDs(mockIOS)) { + String name = _getDeviceName(id, mockIOS); + devices.add(new IOSDevice(id, name: name)); + } + return devices; + } + + static Iterable<String> _getAttachedDeviceIDs([IOSDevice mockIOS]) { + String listerPath = (mockIOS != null) ? mockIOS.listerPath : _checkForCommand('idevice_id'); + try { + String output = runSync([listerPath, '-l']); + return output.trim().split('\n').where((String s) => s != null && s.isNotEmpty); + } catch (e) { + return <String>[]; + } + } + + static String _getDeviceName(String deviceID, [IOSDevice mockIOS]) { + String informerPath = (mockIOS != null) + ? mockIOS.informerPath + : _checkForCommand('ideviceinfo'); + return runSync([informerPath, '-k', 'DeviceName', '-u', deviceID]).trim(); + } + + static final Map<String, String> _commandMap = {}; + static String _checkForCommand( + String command, [ + String macInstructions = _ideviceinstallerInstructions + ]) { + return _commandMap.putIfAbsent(command, () { + try { + command = runCheckedSync(['which', command]).trim(); + } catch (e) { + if (Platform.isMacOS) { + printError('$command not found. $macInstructions'); + } else { + printError('Cannot control iOS devices or simulators. $command is not available on your platform.'); + } + } + return command; + }); + } + + @override + bool installApp(ApplicationPackage app) { + try { + runCheckedSync([installerPath, '-i', app.localPath]); + return true; + } catch (e) { + return false; + } + return false; + } + + @override + bool isConnected() => _getAttachedDeviceIDs().contains(id); + + @override + bool isSupported() => true; + + @override + bool isAppInstalled(ApplicationPackage app) { + try { + String apps = runCheckedSync([installerPath, '--list-apps']); + if (new RegExp(app.id, multiLine: true).hasMatch(apps)) { + return true; + } + } catch (e) { + return false; + } + return false; + } + + @override + Future<bool> startApp( + ApplicationPackage app, + Toolchain toolchain, { + String mainPath, + String route, + bool checked: true, + bool clearLogs: false, + bool startPaused: false, + int debugPort: observatoryDefaultPort, + Map<String, dynamic> platformArgs + }) async { + // TODO(chinmaygarde): Use checked, mainPath, route, clearLogs. + // TODO(devoncarew): Handle startPaused, debugPort. + printTrace('Building ${app.name} for $id'); + + // Step 1: Install the precompiled application if necessary. + bool buildResult = await buildIOSXcodeProject(app, buildForDevice: true); + if (!buildResult) { + printError('Could not build the precompiled application for the device.'); + return false; + } + + // Step 2: Check that the application exists at the specified path. + Directory bundle = new Directory(path.join(app.localPath, 'build', 'Release-iphoneos', 'Runner.app')); + bool bundleExists = bundle.existsSync(); + if (!bundleExists) { + printError('Could not find the built application bundle at ${bundle.path}.'); + return false; + } + + // Step 3: Attempt to install the application on the device. + int installationResult = await runCommandAndStreamOutput([ + '/usr/bin/env', + 'ios-deploy', + '--id', + id, + '--bundle', + bundle.path, + ]); + + if (installationResult != 0) { + printError('Could not install ${bundle.path} on $id.'); + return false; + } + + printTrace('Installation successful.'); + return true; + } + + @override + Future<bool> stopApp(ApplicationPackage app) async { + // Currently we don't have a way to stop an app running on iOS. + return false; + } + + Future<bool> pushFile(ApplicationPackage app, String localFile, String targetFile) async { + if (Platform.isMacOS) { + runSync(<String>[ + pusherPath, + '-t', + '1', + '--bundle_id', + app.id, + '--upload', + localFile, + '--to', + targetFile + ]); + return true; + } else { + return false; + } + return false; + } + + @override + TargetPlatform get platform => TargetPlatform.iOS; + + DeviceLogReader createLogReader() => new _IOSDeviceLogReader(this); +} + +class _IOSDeviceLogReader extends DeviceLogReader { + _IOSDeviceLogReader(this.device); + + final IOSDevice device; + + String get name => device.name; + + // TODO(devoncarew): Support [clear]. + Future<int> logs({ bool clear: false }) async { + if (!device.isConnected()) + return 2; + + return await runCommandAndStreamOutput( + <String>[device.loggerPath], + prefix: '[$name] ', + filter: new RegExp(r'Runner') + ); + } + + int get hashCode => name.hashCode; + + bool operator ==(dynamic other) { + if (identical(this, other)) + return true; + if (other is! _IOSDeviceLogReader) + return false; + return other.name == name; + } +}
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index d2aacc3..ef95ce0 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -2,8 +2,23 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; +import 'dart:convert' show JSON; +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import '../application_package.dart'; +import '../artifacts.dart'; import '../base/context.dart'; import '../base/process.dart'; +import '../globals.dart'; +import '../services.dart'; +import 'setup_xcodeproj.dart'; + +String get homeDirectory => path.absolute(Platform.environment['HOME']); + +// TODO(devoncarew): Refactor functionality into XCode. const int kXcodeRequiredVersionMajor = 7; const int kXcodeRequiredVersionMinor = 2; @@ -51,3 +66,135 @@ return false; } } + +Future<bool> buildIOSXcodeProject(ApplicationPackage app, { bool buildForDevice }) async { + String flutterProjectPath = Directory.current.path; + + if (xcodeProjectRequiresUpdate()) { + printTrace('Initializing the Xcode project.'); + if ((await setupXcodeProjectHarness(flutterProjectPath)) != 0) { + printError('Could not initialize the Xcode project.'); + return false; + } + } else { + updateXcodeLocalProperties(flutterProjectPath); + } + + if (!_validateEngineRevision(app)) + return false; + + if (!_checkXcodeVersion()) + return false; + + // Before the build, all service definitions must be updated and the dylibs + // copied over to a location that is suitable for Xcodebuild to find them. + + await _addServicesToBundle(new Directory(app.localPath)); + + List<String> commands = <String>[ + '/usr/bin/env', 'xcrun', 'xcodebuild', '-target', 'Runner', '-configuration', 'Release' + ]; + + if (buildForDevice) { + commands.addAll(<String>['-sdk', 'iphoneos', '-arch', 'arm64']); + } else { + commands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']); + } + + try { + runCheckedSync(commands, workingDirectory: app.localPath); + return true; + } catch (error) { + return false; + } +} + +final RegExp _xcodeVersionRegExp = new RegExp(r'Xcode (\d+)\..*'); +final String _xcodeRequirement = 'Xcode 7.0 or greater is required to develop for iOS.'; + +bool _checkXcodeVersion() { + if (!Platform.isMacOS) + return false; + try { + String version = runCheckedSync(<String>['xcodebuild', '-version']); + Match match = _xcodeVersionRegExp.firstMatch(version); + if (int.parse(match[1]) < 7) { + printError('Found "${match[0]}". $_xcodeRequirement'); + return false; + } + } catch (e) { + printError('Cannot find "xcodebuid". $_xcodeRequirement'); + return false; + } + return true; +} + +bool _validateEngineRevision(ApplicationPackage app) { + String skyRevision = ArtifactStore.engineRevision; + String iosRevision = _getIOSEngineRevision(app); + + if (iosRevision != skyRevision) { + printError("Error: incompatible sky_engine revision."); + printStatus('sky_engine revision: $skyRevision, iOS engine revision: $iosRevision'); + return false; + } else { + printTrace('sky_engine revision: $skyRevision, iOS engine revision: $iosRevision'); + return true; + } +} + +String _getIOSEngineRevision(ApplicationPackage app) { + File revisionFile = new File(path.join(app.localPath, 'REVISION')); + if (revisionFile.existsSync()) { + return revisionFile.readAsStringSync().trim(); + } else { + return null; + } +} + +Future _addServicesToBundle(Directory bundle) async { + List<Map<String, String>> services = []; + printTrace("Trying to resolve native pub services."); + + // Step 1: Parse the service configuration yaml files present in the service + // pub packages. + await parseServiceConfigs(services); + printTrace("Found ${services.length} service definition(s)."); + + // Step 2: Copy framework dylibs to the correct spot for xcodebuild to pick up. + Directory frameworksDirectory = new Directory(path.join(bundle.path, "Frameworks")); + await _copyServiceFrameworks(services, frameworksDirectory); + + // Step 3: Copy the service definitions manifest at the correct spot for + // xcodebuild to pick up. + File manifestFile = new File(path.join(bundle.path, "ServiceDefinitions.json")); + _copyServiceDefinitionsManifest(services, manifestFile); +} + +Future _copyServiceFrameworks(List<Map<String, String>> services, Directory frameworksDirectory) async { + printTrace("Copying service frameworks to '${path.absolute(frameworksDirectory.path)}'."); + frameworksDirectory.createSync(recursive: true); + for (Map<String, String> service in services) { + String dylibPath = await getServiceFromUrl(service['ios-framework'], service['root'], service['name']); + File dylib = new File(dylibPath); + printTrace("Copying ${dylib.path} into bundle."); + if (!dylib.existsSync()) { + printError("The service dylib '${dylib.path}' does not exist."); + continue; + } + // Shell out so permissions on the dylib are preserved. + runCheckedSync(['/bin/cp', dylib.path, frameworksDirectory.path]); + } +} + +void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File manifest) { + printTrace("Creating service definitions manifest at '${manifest.path}'"); + List<Map<String, String>> jsonServices = services.map((Map<String, String> service) => { + 'name': service['name'], + // Since we have already moved it to the Frameworks directory. Strip away + // the directory and basenames. + 'framework': path.basenameWithoutExtension(service['ios-framework']) + }).toList(); + Map<String, dynamic> json = { 'services' : jsonServices }; + manifest.writeAsStringSync(JSON.encode(json), mode: FileMode.WRITE, flush: true); +}
diff --git a/packages/flutter_tools/lib/src/ios/simulator.dart b/packages/flutter_tools/lib/src/ios/simulator.dart deleted file mode 100644 index 3328191..0000000 --- a/packages/flutter_tools/lib/src/ios/simulator.dart +++ /dev/null
@@ -1,173 +0,0 @@ -// Copyright 2016 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:convert' show JSON; -import 'dart:io'; - -import '../base/process.dart'; -import '../globals.dart'; - -const String _xcrunPath = '/usr/bin/xcrun'; - -const String _simulatorPath = - '/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app/Contents/MacOS/Simulator'; - -/// A wrapper around the `simctl` command line tool. -class SimControl { - static Future<bool> boot({String deviceId}) async { - if (_isAnyConnected()) - return true; - - if (deviceId == null) { - runDetached([_simulatorPath]); - Future<bool> checkConnection([int attempts = 20]) async { - if (attempts == 0) { - printStatus('Timed out waiting for iOS Simulator to boot.'); - return false; - } - if (!_isAnyConnected()) { - printStatus('Waiting for iOS Simulator to boot...'); - return await new Future.delayed(new Duration(milliseconds: 500), - () => checkConnection(attempts - 1) - ); - } - return true; - } - return await checkConnection(); - } else { - try { - runCheckedSync([_xcrunPath, 'simctl', 'boot', deviceId]); - return true; - } catch (e) { - printError('Unable to boot iOS Simulator $deviceId: ', e); - return false; - } - } - - return false; - } - - /// Returns a list of all available devices, both potential and connected. - static List<SimDevice> getDevices() { - // { - // "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" - // }, - // ... - - List<String> args = <String>['simctl', 'list', '--json', 'devices']; - printTrace('$_xcrunPath ${args.join(' ')}'); - ProcessResult results = Process.runSync(_xcrunPath, args); - if (results.exitCode != 0) { - printError('Error executing simctl: ${results.exitCode}\n${results.stderr}'); - return <SimDevice>[]; - } - - List<SimDevice> devices = <SimDevice>[]; - - Map<String, Map<String, dynamic>> data = JSON.decode(results.stdout); - Map<String, dynamic> devicesSection = data['devices']; - - for (String deviceCategory in devicesSection.keys) { - List<dynamic> devicesData = devicesSection[deviceCategory]; - - for (Map<String, String> data in devicesData) { - devices.add(new SimDevice(deviceCategory, data)); - } - } - - return devices; - } - - /// Returns all the connected simulator devices. - static List<SimDevice> getConnectedDevices() { - return getDevices().where((SimDevice device) => device.isBooted).toList(); - } - - static StreamController<List<SimDevice>> _trackDevicesControler; - - /// Listens to changes in the set of connected devices. The implementation - /// currently uses polling. Callers should be careful to call cancel() on any - /// stream subscription when finished. - /// - /// TODO(devoncarew): We could investigate using the usbmuxd protocol directly. - static Stream<List<SimDevice>> trackDevices() { - if (_trackDevicesControler == null) { - Timer timer; - Set<String> deviceIds = new Set<String>(); - - _trackDevicesControler = new StreamController.broadcast( - onListen: () { - timer = new Timer.periodic(new Duration(seconds: 4), (Timer timer) { - List<SimDevice> devices = getConnectedDevices(); - - if (_updateDeviceIds(devices, deviceIds)) { - _trackDevicesControler.add(devices); - } - }); - }, onCancel: () { - timer?.cancel(); - deviceIds.clear(); - } - ); - } - - return _trackDevicesControler.stream; - } - - /// Update the cached set of device IDs and return whether there were any changes. - static bool _updateDeviceIds(List<SimDevice> devices, Set<String> deviceIds) { - Set<String> newIds = new Set<String>.from(devices.map((SimDevice device) => device.udid)); - - bool changed = false; - - for (String id in newIds) { - if (!deviceIds.contains(id)) - changed = true; - } - - for (String id in deviceIds) { - if (!newIds.contains(id)) - changed = true; - } - - deviceIds.clear(); - deviceIds.addAll(newIds); - - return changed; - } - - static bool _isAnyConnected() => getConnectedDevices().isNotEmpty; - - static void install(String deviceId, String appPath) { - runCheckedSync([_xcrunPath, 'simctl', 'install', deviceId, appPath]); - } - - static void launch(String deviceId, String appIdentifier, [List<String> launchArgs]) { - List<String> args = [_xcrunPath, 'simctl', 'launch', deviceId, appIdentifier]; - if (launchArgs != null) - args.addAll(launchArgs); - runCheckedSync(args); - } -} - -class SimDevice { - SimDevice(this.category, this.data); - - final String category; - final Map<String, String> data; - - String get state => data['state']; - String get availability => data['availability']; - String get name => data['name']; - String get udid => data['udid']; - - bool get isBooted => state == 'Booted'; -}
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart new file mode 100644 index 0000000..9480981 --- /dev/null +++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -0,0 +1,488 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert' show JSON; +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import '../application_package.dart'; +import '../base/common.dart'; +import '../base/process.dart'; +import '../build_configuration.dart'; +import '../device.dart'; +import '../globals.dart'; +import '../toolchain.dart'; +import 'mac.dart'; + +const String _xcrunPath = '/usr/bin/xcrun'; + +const String _simulatorPath = + '/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app/Contents/MacOS/Simulator'; + +class IOSSimulators extends PollingDeviceDiscovery { + IOSSimulators() : super('IOSSimulators'); + + bool get supportsPlatform => Platform.isMacOS; + List<Device> pollingGetDevices() => IOSSimulator.getAttachedDevices(); +} + +/// A wrapper around the `simctl` command line tool. +class SimControl { + static Future<bool> boot({String deviceId}) async { + if (_isAnyConnected()) + return true; + + if (deviceId == null) { + runDetached([_simulatorPath]); + Future<bool> checkConnection([int attempts = 20]) async { + if (attempts == 0) { + printStatus('Timed out waiting for iOS Simulator to boot.'); + return false; + } + if (!_isAnyConnected()) { + printStatus('Waiting for iOS Simulator to boot...'); + return await new Future.delayed(new Duration(milliseconds: 500), + () => checkConnection(attempts - 1) + ); + } + return true; + } + return await checkConnection(); + } else { + try { + runCheckedSync([_xcrunPath, 'simctl', 'boot', deviceId]); + return true; + } catch (e) { + printError('Unable to boot iOS Simulator $deviceId: ', e); + return false; + } + } + + return false; + } + + /// Returns a list of all available devices, both potential and connected. + static List<SimDevice> getDevices() { + // { + // "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" + // }, + // ... + + List<String> args = <String>['simctl', 'list', '--json', 'devices']; + printTrace('$_xcrunPath ${args.join(' ')}'); + ProcessResult results = Process.runSync(_xcrunPath, args); + if (results.exitCode != 0) { + printError('Error executing simctl: ${results.exitCode}\n${results.stderr}'); + return <SimDevice>[]; + } + + List<SimDevice> devices = <SimDevice>[]; + + Map<String, Map<String, dynamic>> data = JSON.decode(results.stdout); + Map<String, dynamic> devicesSection = data['devices']; + + for (String deviceCategory in devicesSection.keys) { + List<dynamic> devicesData = devicesSection[deviceCategory]; + + for (Map<String, String> data in devicesData) { + devices.add(new SimDevice(deviceCategory, data)); + } + } + + return devices; + } + + /// Returns all the connected simulator devices. + static List<SimDevice> getConnectedDevices() { + return getDevices().where((SimDevice device) => device.isBooted).toList(); + } + + static StreamController<List<SimDevice>> _trackDevicesControler; + + /// Listens to changes in the set of connected devices. The implementation + /// currently uses polling. Callers should be careful to call cancel() on any + /// stream subscription when finished. + /// + /// TODO(devoncarew): We could investigate using the usbmuxd protocol directly. + static Stream<List<SimDevice>> trackDevices() { + if (_trackDevicesControler == null) { + Timer timer; + Set<String> deviceIds = new Set<String>(); + + _trackDevicesControler = new StreamController.broadcast( + onListen: () { + timer = new Timer.periodic(new Duration(seconds: 4), (Timer timer) { + List<SimDevice> devices = getConnectedDevices(); + + if (_updateDeviceIds(devices, deviceIds)) { + _trackDevicesControler.add(devices); + } + }); + }, onCancel: () { + timer?.cancel(); + deviceIds.clear(); + } + ); + } + + return _trackDevicesControler.stream; + } + + /// Update the cached set of device IDs and return whether there were any changes. + static bool _updateDeviceIds(List<SimDevice> devices, Set<String> deviceIds) { + Set<String> newIds = new Set<String>.from(devices.map((SimDevice device) => device.udid)); + + bool changed = false; + + for (String id in newIds) { + if (!deviceIds.contains(id)) + changed = true; + } + + for (String id in deviceIds) { + if (!newIds.contains(id)) + changed = true; + } + + deviceIds.clear(); + deviceIds.addAll(newIds); + + return changed; + } + + static bool _isAnyConnected() => getConnectedDevices().isNotEmpty; + + static void install(String deviceId, String appPath) { + runCheckedSync([_xcrunPath, 'simctl', 'install', deviceId, appPath]); + } + + static void launch(String deviceId, String appIdentifier, [List<String> launchArgs]) { + List<String> args = [_xcrunPath, 'simctl', 'launch', deviceId, appIdentifier]; + if (launchArgs != null) + args.addAll(launchArgs); + runCheckedSync(args); + } +} + +class SimDevice { + SimDevice(this.category, this.data); + + final String category; + final Map<String, String> data; + + String get state => data['state']; + String get availability => data['availability']; + String get name => data['name']; + String get udid => data['udid']; + + bool get isBooted => state == 'Booted'; +} + +class IOSSimulator extends Device { + IOSSimulator(String id, { this.name }) : super(id); + + static List<IOSSimulator> getAttachedDevices() { + if (!xcode.isInstalledAndMeetsVersionCheck) + return <IOSSimulator>[]; + + return SimControl.getConnectedDevices().map((SimDevice device) { + return new IOSSimulator(device.udid, name: device.name); + }).toList(); + } + + final String name; + + String get xcrunPath => path.join('/usr', 'bin', 'xcrun'); + + String _getSimulatorPath() { + return path.join(homeDirectory, 'Library', 'Developer', 'CoreSimulator', 'Devices', id); + } + + String _getSimulatorAppHomeDirectory(ApplicationPackage app) { + String simulatorPath = _getSimulatorPath(); + if (simulatorPath == null) + return null; + return path.join(simulatorPath, 'data'); + } + + @override + bool installApp(ApplicationPackage app) { + if (!isConnected()) + return false; + + try { + SimControl.install(id, app.localPath); + return true; + } catch (e) { + return false; + } + } + + @override + bool isConnected() { + if (!Platform.isMacOS) + return false; + return SimControl.getConnectedDevices().any((SimDevice device) => device.udid == id); + } + + @override + bool isSupported() { + if (!Platform.isMacOS) { + _supportMessage = "Not supported on a non Mac host"; + return false; + } + + // Step 1: Check if the device is part of a blacklisted category. + // We do not support WatchOS or tvOS devices. + + RegExp blacklist = new RegExp(r'Apple (TV|Watch)', caseSensitive: false); + + if (blacklist.hasMatch(name)) { + _supportMessage = "Flutter does not support either the Apple TV or Watch. Choose an iPhone 5s or above."; + return false; + } + + // Step 2: Check if the device must be rejected because of its version. + // There is an artitifical check on older simulators where arm64 + // targetted applications cannot be run (even though the + // Flutter runner on the simulator is completely different). + + RegExp versionExp = new RegExp(r'iPhone ([0-9])+'); + Match match = versionExp.firstMatch(name); + + if (match == null) { + // Not an iPhone. All available non-iPhone simulators are compatible. + return true; + } + + if (int.parse(match.group(1)) > 5) { + // iPhones 6 and above are always fine. + return true; + } + + // The 's' subtype of 5 is compatible. + if (name.contains('iPhone 5s')) { + return true; + } + + _supportMessage = "The simulator version is too old. Choose an iPhone 5s or above."; + return false; + } + + String _supportMessage; + + @override + String supportMessage() { + if (isSupported()) { + return "Supported"; + } + + return _supportMessage != null ? _supportMessage : "Unknown"; + } + + @override + bool isAppInstalled(ApplicationPackage app) { + try { + String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app); + return FileSystemEntity.isDirectorySync(simulatorHomeDirectory); + } catch (e) { + return false; + } + } + + @override + Future<bool> startApp( + ApplicationPackage app, + Toolchain toolchain, { + String mainPath, + String route, + bool checked: true, + bool clearLogs: false, + bool startPaused: false, + int debugPort: observatoryDefaultPort, + Map<String, dynamic> platformArgs + }) async { + // TODO(chinmaygarde): Use mainPath, route. + printTrace('Building ${app.name} for $id.'); + + if (clearLogs) + this.clearLogs(); + + // Step 1: Build the Xcode project. + bool buildResult = await buildIOSXcodeProject(app, buildForDevice: false); + if (!buildResult) { + printError('Could not build the application for the simulator.'); + return false; + } + + // Step 2: Assert that the Xcode project was successfully built. + Directory bundle = new Directory(path.join(app.localPath, 'build', 'Release-iphonesimulator', 'Runner.app')); + bool bundleExists = await bundle.exists(); + if (!bundleExists) { + printError('Could not find the built application bundle at ${bundle.path}.'); + return false; + } + + // Step 3: Install the updated bundle to the simulator. + SimControl.install(id, path.absolute(bundle.path)); + + // Step 4: Prepare launch arguments. + List<String> args = <String>[]; + + if (checked) + args.add("--enable-checked-mode"); + + if (startPaused) + args.add("--start-paused"); + + if (debugPort != observatoryDefaultPort) + args.add("--observatory-port=$debugPort"); + + // Step 5: Launch the updated application in the simulator. + try { + SimControl.launch(id, app.id, args); + } catch (error) { + printError('$error'); + return false; + } + + printTrace('Successfully started ${app.name} on $id.'); + + return true; + } + + @override + Future<bool> stopApp(ApplicationPackage app) async { + // Currently we don't have a way to stop an app running on iOS. + return false; + } + + Future<bool> pushFile( + ApplicationPackage app, String localFile, String targetFile) async { + if (Platform.isMacOS) { + String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app); + runCheckedSync(<String>['cp', localFile, path.join(simulatorHomeDirectory, targetFile)]); + return true; + } + return false; + } + + String get logFilePath { + return path.join(homeDirectory, 'Library', 'Logs', 'CoreSimulator', id, 'system.log'); + } + + @override + TargetPlatform get platform => TargetPlatform.iOSSimulator; + + DeviceLogReader createLogReader() => new _IOSSimulatorLogReader(this); + + void clearLogs() { + File logFile = new File(logFilePath); + if (logFile.existsSync()) { + RandomAccessFile randomFile = logFile.openSync(mode: FileMode.WRITE); + randomFile.truncateSync(0); + randomFile.closeSync(); + } + } + + void ensureLogsExists() { + File logFile = new File(logFilePath); + if (!logFile.existsSync()) + logFile.writeAsBytesSync(<int>[]); + } +} + +class _IOSSimulatorLogReader extends DeviceLogReader { + _IOSSimulatorLogReader(this.device); + + final IOSSimulator device; + + bool _lastWasFiltered = false; + + String get name => device.name; + + Future<int> logs({ bool clear: false }) async { + if (!device.isConnected()) + return 2; + + if (clear) + device.clearLogs(); + + device.ensureLogsExists(); + + // Match the log prefix (in order to shorten it): + // 'Jan 29 01:31:44 devoncarew-macbookpro3 SpringBoard[96648]: ...' + RegExp mapRegex = new RegExp(r'\S+ +\S+ +\S+ \S+ (.+)\[\d+\]\)?: (.*)$'); + // Jan 31 19:23:28 --- last message repeated 1 time --- + RegExp lastMessageRegex = new RegExp(r'\S+ +\S+ +\S+ --- (.*) ---$'); + + // This filter matches many Flutter lines in the log: + // new RegExp(r'(FlutterRunner|flutter.runner.Runner|$id)'), but it misses + // a fair number, including ones that would be useful in diagnosing crashes. + // For now, we're not filtering the log file (but do clear it with each run). + + Future<int> result = runCommandAndStreamOutput( + <String>['tail', '-n', '+0', '-F', device.logFilePath], + prefix: '[$name] ', + mapFunction: (String string) { + Match match = mapRegex.matchAsPrefix(string); + if (match != null) { + _lastWasFiltered = true; + + // Filter out some messages that clearly aren't related to Flutter. + if (string.contains(': could not find icon for representation -> com.apple.')) + return null; + String category = match.group(1); + String content = match.group(2); + if (category == 'Game Center' || category == 'itunesstored' || category == 'nanoregistrylaunchd' || + category == 'mstreamd' || category == 'syncdefaultsd' || category == 'companionappd' || + category == 'searchd') + return null; + + _lastWasFiltered = false; + + if (category == 'Runner') + return content; + return '$category: $content'; + } + match = lastMessageRegex.matchAsPrefix(string); + if (match != null && !_lastWasFiltered) + return '(${match.group(1)})'; + return string; + } + ); + + // Track system.log crashes. + // ReportCrash[37965]: Saved crash report for FlutterRunner[37941]... + runCommandAndStreamOutput( + <String>['tail', '-F', '/private/var/log/system.log'], + prefix: '[$name] ', + filter: new RegExp(r' FlutterRunner\[\d+\] '), + mapFunction: (String string) { + Match match = mapRegex.matchAsPrefix(string); + return match == null ? string : '${match.group(1)}: ${match.group(2)}'; + } + ); + + return await result; + } + + int get hashCode => device.logFilePath.hashCode; + + bool operator ==(dynamic other) { + if (identical(this, other)) + return true; + if (other is! _IOSSimulatorLogReader) + return false; + return other.device.logFilePath == device.logFilePath; + } +}