Merge pull request #1517 from yjbanov/overflow-box-debugFillDescription-1487

add debugFillDescription to OverflowBox
diff --git a/bin/flutter b/bin/flutter
index 3803576..1ec69b6 100755
--- a/bin/flutter
+++ b/bin/flutter
@@ -7,6 +7,7 @@
 
 export FLUTTER_ROOT=$(dirname $(dirname "${BASH_SOURCE[0]}"))
 FLUTTER_TOOLS_DIR="$FLUTTER_ROOT/packages/flutter_tools"
+FLUTTER_DIR="$FLUTTER_ROOT/packages/flutter"
 SNAPSHOT_PATH="$FLUTTER_ROOT/bin/cache/flutter_tools.snapshot"
 STAMP_PATH="$FLUTTER_ROOT/bin/cache/flutter_tools.stamp"
 SCRIPT_PATH="$FLUTTER_TOOLS_DIR/bin/flutter_tools.dart"
@@ -18,6 +19,7 @@
 if [ ! -f "$SNAPSHOT_PATH" ] || [ ! -f "$STAMP_PATH" ] || [ `cat "$STAMP_PATH"` != "$REVISION" ] || [ "$FLUTTER_TOOLS_DIR/pubspec.yaml" -nt "$FLUTTER_TOOLS_DIR/pubspec.lock" ]; then
   echo Updating flutter tool...
   (cd "$FLUTTER_TOOLS_DIR"; pub get > /dev/null)
+  (cd "$FLUTTER_DIR"; pub get > /dev/null)  # Allows us to check if sky_engine's REVISION is correct
   $DART --snapshot="$SNAPSHOT_PATH" --package-root="$FLUTTER_TOOLS_DIR/packages" "$SCRIPT_PATH"
   echo -n $REVISION > "$STAMP_PATH"
 fi
diff --git a/bin/flutter.bat b/bin/flutter.bat
index 4d8580e..d047aa6 100644
--- a/bin/flutter.bat
+++ b/bin/flutter.bat
@@ -6,6 +6,7 @@
 SETLOCAL ENABLEDELAYEDEXPANSION
 FOR %%i IN ("%~dp0..") DO SET "flutter_root=%%~fi" REM Get the parent directory
 SET flutter_tools_dir=%flutter_root%\packages\flutter_tools
+SET flutter_dir=%flutter_root%\packages\flutter
 SET snapshot_path=%flutter_root%\bin\cache\flutter_tools.snapshot
 SET stamp_path=%flutter_root%\bin\cache\flutter_tools.stamp
 SET script_path=%flutter_tools_dir%\bin\flutter_tools.dart
@@ -33,6 +34,9 @@
 CD "%flutter_tools_dir%"
 ECHO Updating flutter tool...
 CALL pub.bat get
+CD "%flutter_dir"
+REM Allows us to check if sky_engine's REVISION is correct
+CALL pub.bat get
 CD "%flutter_root%"
 CALL %dart% --snapshot="%snapshot_path%" --package-root="%flutter_tools_dir%\packages" "%script_path%"
 <nul SET /p=%revision%> "%stamp_path%"
diff --git a/packages/flutter_tools/lib/src/android/device_android.dart b/packages/flutter_tools/lib/src/android/device_android.dart
index 6d94c5a..934d4cd 100644
--- a/packages/flutter_tools/lib/src/android/device_android.dart
+++ b/packages/flutter_tools/lib/src/android/device_android.dart
@@ -294,6 +294,7 @@
     String mainPath,
     String route,
     bool checked: true,
+    bool clearLogs: false,
     Map<String, dynamic> platformArgs
   }) {
     return flx.buildInTempDir(
@@ -309,7 +310,7 @@
           checked: checked,
           traceStartup: platformArgs['trace-startup'],
           route: route,
-          clearLogs: platformArgs['clear-logs']
+          clearLogs: clearLogs
         )) {
           return true;
         } else {
@@ -334,26 +335,7 @@
     runSync(adbCommandForDevice(['logcat', '-c']));
   }
 
-  Future<int> logs({bool clear: false}) async {
-    if (!isConnected()) {
-      return 2;
-    }
-
-    if (clear) {
-      clearLogs();
-    }
-
-    return await runCommandAndStreamOutput(adbCommandForDevice([
-      'logcat',
-      '-v',
-      'tag', // Only log the tag and the message
-      '-s',
-      'flutter:V',
-      'ActivityManager:W',
-      'System.err:W',
-      '*:F',
-    ]), prefix: 'android: ');
-  }
+  DeviceLogReader createLogReader() => new _AdbLogReader(this);
 
   void startTracing(AndroidApk apk) {
     runCheckedSync(adbCommandForDevice([
@@ -529,3 +511,41 @@
     return _defaultAdbPath;
   }
 }
+
+/// A log reader that logs from `adb logcat`. This will have the same output as
+/// another copy of [_AdbLogReader], and the two instances will be equivalent.
+class _AdbLogReader extends DeviceLogReader {
+  _AdbLogReader(this.device);
+
+  final AndroidDevice device;
+
+  String get name => 'Android';
+
+  Future<int> logs({bool clear: false}) async {
+    if (!device.isConnected())
+      return 2;
+
+    if (clear)
+      device.clearLogs();
+
+    return await runCommandAndStreamOutput(device.adbCommandForDevice(<String>[
+      'logcat',
+      '-v',
+      'tag', // Only log the tag and the message
+      '-s',
+      'flutter:V',
+      'ActivityManager:W',
+      'System.err:W',
+      '*:F',
+    ]), prefix: '[Android] ');
+  }
+
+  // Intentionally constant; overridden because we've overridden the `operator ==` method below.
+  int get hashCode => name.hashCode;
+
+  bool operator ==(dynamic other) {
+    if (identical(this, other))
+      return true;
+    return other is _AdbLogReader;
+  }
+}
diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart
index 5af0f3c..123b752 100644
--- a/packages/flutter_tools/lib/src/artifacts.dart
+++ b/packages/flutter_tools/lib/src/artifacts.dart
@@ -192,12 +192,15 @@
     }
   }
 
-  static void ensureHasSkyEnginePackage() {
-    Directory skyEnginePackage = new Directory(path.join(packageRoot, 'sky_engine'));
-    if (!skyEnginePackage.existsSync()) {
+  static void validateSkyEnginePackage() {
+    if (engineRevision == null) {
       printError("Cannot locate the sky_engine package; did you include 'flutter' in your pubspec.yaml file?");
       throw new ProcessExit(2);
     }
+    if (engineRevision != expectedEngineRevision) {
+      printError("Error: incompatible sky_engine package; please run 'pub get' to get the correct one.\n");
+      throw new ProcessExit(2);
+    }
   }
 
   static String _engineRevision;
@@ -211,6 +214,18 @@
     return _engineRevision;
   }
 
+  static String _expectedEngineRevision;
+
+  static String get expectedEngineRevision {
+    if (_expectedEngineRevision == null) {
+      // TODO(jackson): Parse the .packages file and use the path from there instead
+      File revisionFile = new File(path.join(flutterRoot, 'packages', 'flutter', 'packages', 'sky_engine', 'REVISION'));
+      if (revisionFile.existsSync())
+        _expectedEngineRevision = revisionFile.readAsStringSync();
+    }
+    return _expectedEngineRevision;
+  }
+
   static String getCloudStorageBaseUrl(String platform) {
     return _getCloudStorageBaseUrl(
       platform: platform,
@@ -295,7 +310,7 @@
   }
 
   static Directory _getCacheDirForPlatform(String platform) {
-    ensureHasSkyEnginePackage();
+    validateSkyEnginePackage();
     Directory baseDir = _getBaseCacheDir();
     // TODO(jamesr): Add support for more configurations.
     String config = 'Release';
diff --git a/packages/flutter_tools/lib/src/base/process.dart b/packages/flutter_tools/lib/src/base/process.dart
index bc9b8c8..7da7699 100644
--- a/packages/flutter_tools/lib/src/base/process.dart
+++ b/packages/flutter_tools/lib/src/base/process.dart
@@ -8,12 +8,15 @@
 
 import 'context.dart';
 
+typedef String StringConverter(String string);
+
 /// This runs the command and streams stdout/stderr from the child process to
 /// this process' stdout/stderr.
 Future<int> runCommandAndStreamOutput(List<String> cmd, {
+  String workingDirectory,
   String prefix: '',
   RegExp filter,
-  String workingDirectory
+  StringConverter mapFunction
 }) async {
   printTrace(cmd.join(' '));
   Process process = await Process.start(
@@ -26,14 +29,20 @@
     .transform(const LineSplitter())
     .where((String line) => filter == null ? true : filter.hasMatch(line))
     .listen((String line) {
-      printStatus('$prefix$line');
+      if (mapFunction != null)
+        line = mapFunction(line);
+      if (line != null)
+        printStatus('$prefix$line');
     });
   process.stderr
     .transform(UTF8.decoder)
     .transform(const LineSplitter())
     .where((String line) => filter == null ? true : filter.hasMatch(line))
     .listen((String line) {
-      printError('$prefix$line');
+      if (mapFunction != null)
+        line = mapFunction(line);
+      if (line != null)
+        printError('$prefix$line');
     });
   return await process.exitCode;
 }
@@ -57,7 +66,7 @@
 /// Run cmd and return stdout.
 /// Throws an error if cmd exits with a non-zero value.
 String runCheckedSync(List<String> cmd, { String workingDirectory }) {
-  return _runWithLoggingSync(cmd, workingDirectory: workingDirectory, checked: true);
+  return _runWithLoggingSync(cmd, workingDirectory: workingDirectory, checked: true, noisyErrors: true);
 }
 
 /// Run cmd and return stdout.
@@ -73,6 +82,7 @@
 
 String _runWithLoggingSync(List<String> cmd, {
   bool checked: false,
+  bool noisyErrors: false,
   String workingDirectory
 }) {
   printTrace(cmd.join(' '));
@@ -82,8 +92,13 @@
     String errorDescription = 'Error code ${results.exitCode} '
         'returned when attempting to run command: ${cmd.join(' ')}';
     printTrace(errorDescription);
-    if (results.stderr.length > 0)
-      printTrace('Errors logged: ${results.stderr.trim()}');
+    if (results.stderr.length > 0) {
+      if (noisyErrors) {
+        printError(results.stderr.trim());
+      } else {
+        printTrace('Errors logged: ${results.stderr.trim()}');
+      }
+    }
     if (checked)
       throw errorDescription;
   }
diff --git a/packages/flutter_tools/lib/src/commands/ios.dart b/packages/flutter_tools/lib/src/commands/ios.dart
index 3a9d7e7..b424981 100644
--- a/packages/flutter_tools/lib/src/commands/ios.dart
+++ b/packages/flutter_tools/lib/src/commands/ios.dart
@@ -17,8 +17,6 @@
   final String name = "ios";
   final String description = "Commands for creating and updating Flutter iOS projects.";
 
-  final bool requiresProjectRoot = true;
-
   IOSCommand() {
     argParser.addFlag('init', help: 'Initialize the Xcode project for building the iOS application');
   }
diff --git a/packages/flutter_tools/lib/src/commands/logs.dart b/packages/flutter_tools/lib/src/commands/logs.dart
index a98e3ab..0b274d3 100644
--- a/packages/flutter_tools/lib/src/commands/logs.dart
+++ b/packages/flutter_tools/lib/src/commands/logs.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async';
 
+import '../base/context.dart';
 import '../device.dart';
 import '../runner/flutter_command.dart';
 
@@ -13,25 +14,52 @@
 
   LogsCommand() {
     argParser.addFlag('clear',
-        negatable: false,
-        abbr: 'c',
-        help: 'Clear log history before reading from logs (Android only).');
+      negatable: false,
+      abbr: 'c',
+      help: 'Clear log history before reading from logs.'
+    );
   }
 
   bool get requiresProjectRoot => false;
 
-  @override
   Future<int> runInProject() async {
-    connectToDevices();
+    DeviceManager deviceManager = new DeviceManager();
+    List<Device> devices;
+
+    String deviceId = globalResults['device-id'];
+    if (deviceId != null) {
+      Device device = await deviceManager.getDeviceById(deviceId);
+      if (device == null) {
+        printError("No device found with id '$deviceId'.");
+        return 1;
+      }
+      devices = <Device>[device];
+    } else {
+      devices = await deviceManager.getDevices();
+    }
+
+    if (devices.isEmpty) {
+      printStatus('No connected devices.');
+      return 0;
+    }
 
     bool clear = argResults['clear'];
 
-    Iterable<Future<int>> results = devices.all.map(
-        (Device device) => device.logs(clear: clear));
+    Set<DeviceLogReader> readers = new Set<DeviceLogReader>();
+    for (Device device in devices) {
+      readers.add(device.createLogReader());
+    }
 
-    for (Future<int> result in results)
-      await result;
+    printStatus('Logging for ${readers.join(', ')}...');
 
-    return 0;
+    List<int> results = await Future.wait(readers.map((DeviceLogReader reader) async {
+      int result = await reader.logs(clear: clear);
+      if (result != 0)
+        printError('Error listening to $reader logs.');
+      return result;
+    }));
+
+    // If all readers failed, return an error.
+    return results.every((int result) => result != 0) ? 1 : 0;
   }
 }
diff --git a/packages/flutter_tools/lib/src/commands/start.dart b/packages/flutter_tools/lib/src/commands/start.dart
index 121104a..60f3c60 100644
--- a/packages/flutter_tools/lib/src/commands/start.dart
+++ b/packages/flutter_tools/lib/src/commands/start.dart
@@ -134,8 +134,6 @@
 
     if (traceStartup != null)
       platformArgs['trace-startup'] = traceStartup;
-    if (clearLogs != null)
-      platformArgs['clear-logs'] = clearLogs;
 
     printStatus('Starting ${_getDisplayPath(mainPath)} on ${device.name}...');
 
@@ -145,6 +143,7 @@
       mainPath: mainPath,
       route: route,
       checked: checked,
+      clearLogs: clearLogs,
       platformArgs: platformArgs
     );
 
diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart
index 4ca8119..127391b 100644
--- a/packages/flutter_tools/lib/src/commands/test.dart
+++ b/packages/flutter_tools/lib/src/commands/test.dart
@@ -20,13 +20,18 @@
 
   bool get requiresProjectRoot => false;
 
-  String get projectRootValidationErrorMessage {
-    return 'Error: No pubspec.yaml file found.\n'
-      'If you wish to run the tests in the Flutter repository\'s \'flutter\' package,\n'
-      'pass --flutter-repo before any test paths. Otherwise, run this command from the\n'
-      'root of your project. Test files must be called *_test.dart and must reside in\n'
-      'the package\'s \'test\' directory (or one of its subdirectories).';
-  }
+  @override
+  Validator projectRootValidator = () {
+    if (!FileSystemEntity.isFileSync('pubspec.yaml')) {
+      printError('Error: No pubspec.yaml file found.\n'
+        'If you wish to run the tests in the Flutter repository\'s \'flutter\' package,\n'
+        'pass --flutter-repo before any test paths. Otherwise, run this command from the\n'
+        'root of your project. Test files must be called *_test.dart and must reside in\n'
+        'the package\'s \'test\' directory (or one of its subdirectories).');
+      return false;
+    }
+    return true;
+  };
 
   Future<String> _getShellPath(BuildConfiguration config) async {
     if (config.type == BuildType.prebuilt) {
@@ -80,7 +85,7 @@
     List<String> testArgs = argResults.rest.map((String testPath) => path.absolute(testPath)).toList();
 
     final bool runFlutterTests = argResults['flutter-repo'];
-    if (!runFlutterTests && !validateProjectRoot())
+    if (!runFlutterTests && !projectRootValidator())
       return 1;
 
     // If we're running the flutter tests, we want to use the packages directory
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index b89b786..4de713e 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -34,6 +34,19 @@
 
   Completer _initedCompleter = new Completer();
 
+  /// Return the device with the matching ID; else, complete the Future with
+  /// `null`.
+  ///
+  /// This does a case insentitive compare with `deviceId`.
+  Future<Device> getDeviceById(String deviceId) async {
+    deviceId = deviceId.toLowerCase();
+    List<Device> devices = await getDevices();
+    return devices.firstWhere(
+      (Device device) => device.id.toLowerCase() == deviceId,
+      orElse: () => null
+    );
+  }
+
   Future<List<Device>> getDevices() async {
     await _initedCompleter.future;
 
@@ -78,7 +91,7 @@
 
   TargetPlatform get platform;
 
-  Future<int> logs({bool clear: false});
+  DeviceLogReader createLogReader();
 
   /// Start an app package on the current device.
   ///
@@ -90,6 +103,7 @@
     String mainPath,
     String route,
     bool checked: true,
+    bool clearLogs: false,
     Map<String, dynamic> platformArgs
   });
 
@@ -99,6 +113,21 @@
   String toString() => '$runtimeType $id';
 }
 
+/// Read the log for a particular device. Subclasses must implement `hashCode`
+/// and `operator ==` so that log readers that read from the same location can be
+/// de-duped. For example, two Android devices will both try and log using
+/// `adb logcat`; we don't want to display two identical log streams.
+abstract class DeviceLogReader {
+  String get name;
+
+  Future<int> logs({ bool clear: false });
+
+  int get hashCode;
+  bool operator ==(dynamic other);
+
+  String toString() => name;
+}
+
 // TODO(devoncarew): Unify this with [DeviceManager].
 class DeviceStore {
   final AndroidDevice android;
diff --git a/packages/flutter_tools/lib/src/ios/device_ios.dart b/packages/flutter_tools/lib/src/ios/device_ios.dart
index 0b9e549..5a48a9e 100644
--- a/packages/flutter_tools/lib/src/ios/device_ios.dart
+++ b/packages/flutter_tools/lib/src/ios/device_ios.dart
@@ -184,9 +184,10 @@
     String mainPath,
     String route,
     bool checked: true,
+    bool clearLogs: false,
     Map<String, dynamic> platformArgs
   }) async {
-    // TODO: Use checked, mainPath, route
+    // TODO(chinmaygarde): Use checked, mainPath, route, clearLogs.
     printTrace('Building ${app.name} for $id');
 
     // Step 1: Install the precompiled application if necessary
@@ -231,8 +232,7 @@
     return false;
   }
 
-  Future<bool> pushFile(
-      ApplicationPackage app, String localFile, String targetFile) async {
+  Future<bool> pushFile(ApplicationPackage app, String localFile, String targetFile) async {
     if (Platform.isMacOS) {
       runSync([
         pusherPath,
@@ -255,14 +255,7 @@
   @override
   TargetPlatform get platform => TargetPlatform.iOS;
 
-  /// Note that clear is not supported on iOS at this time.
-  Future<int> logs({bool clear: false}) async {
-    if (!isConnected()) {
-      return 2;
-    }
-    return await runCommandAndStreamOutput([loggerPath],
-        prefix: 'iOS: ', filter: new RegExp(r'(FlutterRunner|flutter.runner.Runner)'));
-  }
+  DeviceLogReader createLogReader() => new _IOSDeviceLogReader(this);
 }
 
 class IOSSimulator extends Device {
@@ -335,10 +328,9 @@
 
   String _getSimulatorPath() {
     String deviceID = id == defaultDeviceID ? _getRunningSimulatorInfo()?.id : id;
-    String homeDirectory = path.absolute(Platform.environment['HOME']);
     if (deviceID == null)
       return null;
-    return path.join(homeDirectory, 'Library', 'Developer', 'CoreSimulator', 'Devices', deviceID);
+    return path.join(_homeDirectory, 'Library', 'Developer', 'CoreSimulator', 'Devices', deviceID);
   }
 
   String _getSimulatorAppHomeDirectory(ApplicationPackage app) {
@@ -438,11 +430,15 @@
     String mainPath,
     String route,
     bool checked: true,
+    bool clearLogs: false,
     Map<String, dynamic> platformArgs
   }) async {
-    // TODO: Use checked, mainPath, route
+    // TODO(chinmaygarde): Use checked, mainPath, route.
     printTrace('Building ${app.name} for $id');
 
+    if (clearLogs)
+      this.clearLogs();
+
     // Step 1: Build the Xcode project
     bool buildResult = await _buildIOSXcodeProject(app, false);
     if (!buildResult) {
@@ -506,35 +502,123 @@
     return false;
   }
 
+  String get logFilePath {
+    return path.join(_homeDirectory, 'Library', 'Logs', 'CoreSimulator', id, 'system.log');
+  }
+
   @override
   TargetPlatform get platform => TargetPlatform.iOSSimulator;
 
-  Future<int> logs({bool clear: false}) async {
-    if (!isConnected())
+  DeviceLogReader createLogReader() => new _IOSSimulatorLogReader(this);
+
+  void clearLogs() {
+    File logFile = new File(logFilePath);
+    if (logFile.existsSync())
+      logFile.delete();
+  }
+}
+
+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;
 
-    String homeDirectory = path.absolute(Platform.environment['HOME']);
-    String simulatorDeviceID = _getRunningSimulatorInfo().id;
-    String logFilePath = path.join(
-      homeDirectory, 'Library', 'Logs', 'CoreSimulator', simulatorDeviceID, 'system.log'
-    );
-
-    if (clear)
-      runSync(['rm', logFilePath]);
-
-    // TODO(devoncarew): The log message prefix could be shortened or removed.
-    // Jan 29 01:31:44 devoncarew-macbookpro3 SpringBoard[96648]:
-    // TODO(devoncarew): This truncates multi-line messages like:
-    // Jan 29 01:31:43 devoncarew-macbookpro3 CoreSimulatorBridge[96656]: Requesting... {
-    //     environment =     {
-    //     };
-    //   }
     return await runCommandAndStreamOutput(
-      ['tail', '-f', logFilePath],
-      prefix: 'iOS: ',
+      [device.loggerPath],
+      prefix: '[$name] ',
       filter: new RegExp(r'(FlutterRunner|flutter.runner.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;
+
+  String get name => device.name;
+
+  Future<int> logs({bool clear: false}) async {
+    if (!device.isConnected())
+      return 2;
+
+    if (clear)
+      device.clearLogs();
+
+    // 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) {
+          // 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')
+            return null;
+          return '$category: $content';
+        }
+        match = lastMessageRegex.matchAsPrefix(string);
+        if (match != null)
+          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 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;
+  }
 }
 
 class _IOSSimulatorInfo {
@@ -547,6 +631,8 @@
 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;
@@ -573,15 +659,18 @@
   if (!_checkXcodeVersion())
     return false;
 
-  List<String> command = [
-    'xcrun', 'xcodebuild', '-target', 'Runner', '-configuration', 'Release'
+  List<String> commands = [
+    '/usr/bin/env', 'xcrun', 'xcodebuild', '-target', 'Runner', '-configuration', 'Release'
   ];
 
   if (!isDevice) {
-    command.addAll(['-sdk', 'iphonesimulator']);
+    commands.addAll(['-sdk', 'iphonesimulator']);
   }
 
-  ProcessResult result = Process.runSync('/usr/bin/env', command,
-      workingDirectory: app.localPath);
-  return result.exitCode == 0;
+  try {
+    runCheckedSync(commands, workingDirectory: app.localPath);
+    return true;
+  } catch (error) {
+    return false;
+  }
 }
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index 5cab91d..4df0131 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -10,22 +10,19 @@
 import '../application_package.dart';
 import '../base/context.dart';
 import '../build_configuration.dart';
+import '../artifacts.dart';
 import '../device.dart';
 import '../toolchain.dart';
 import 'flutter_command_runner.dart';
 
+typedef bool Validator();
+
 abstract class FlutterCommand extends Command {
   FlutterCommandRunner get runner => super.runner;
 
   /// Whether this command needs to be run from the root of a project.
   bool get requiresProjectRoot => true;
 
-  String get projectRootValidationErrorMessage {
-    return 'Error: No pubspec.yaml file found.\n'
-      'This command should be run from the root of your Flutter project. Do not run\n'
-      'this command from the root of your git clone of Flutter.';
-  }
-
   List<BuildConfiguration> get buildConfigurations => runner.buildConfigurations;
 
   Future downloadApplicationPackages() async {
@@ -49,18 +46,22 @@
   }
 
   Future<int> run() async {
-    if (requiresProjectRoot && !validateProjectRoot())
+    if (requiresProjectRoot && !projectRootValidator())
       return 1;
     return await runInProject();
   }
 
-  bool validateProjectRoot() {
+  // This is a field so that you can modify the value for testing.
+  Validator projectRootValidator = () {
     if (!FileSystemEntity.isFileSync('pubspec.yaml')) {
-      printError(projectRootValidationErrorMessage);
+      printError('Error: No pubspec.yaml file found.\n'
+        'This command should be run from the root of your Flutter project.\n'
+        'Do not run this command from the root of your git clone of Flutter.');
       return false;
     }
+    ArtifactStore.validateSkyEnginePackage();
     return true;
-  }
+  };
 
   Future<int> runInProject();
 
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
index d2e6d18..079b20c 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
@@ -142,11 +142,16 @@
   String get _defaultFlutterRoot {
     if (Platform.environment.containsKey(kFlutterRootEnvironmentVariableName))
       return Platform.environment[kFlutterRootEnvironmentVariableName];
-    String script = Platform.script.toFilePath();
-    if (path.basename(script) == kSnapshotFileName)
-      return path.dirname(path.dirname(path.dirname(script)));
-    if (path.basename(script) == kFlutterToolsScriptFileName)
-      return path.dirname(path.dirname(path.dirname(path.dirname(script))));
+
+    try {
+      String script = Platform.script.toFilePath();
+      if (path.basename(script) == kSnapshotFileName)
+        return path.dirname(path.dirname(path.dirname(script)));
+      if (path.basename(script) == kFlutterToolsScriptFileName)
+        return path.dirname(path.dirname(path.dirname(path.dirname(script))));
+    } catch (error) {
+      printTrace('Unable to locate fluter root: $error');
+    }
     return '.';
   }
 
diff --git a/packages/flutter_tools/test/logs_test.dart b/packages/flutter_tools/test/logs_test.dart
index a7736a6..1e5590b 100644
--- a/packages/flutter_tools/test/logs_test.dart
+++ b/packages/flutter_tools/test/logs_test.dart
@@ -4,7 +4,7 @@
 
 import 'package:args/command_runner.dart';
 import 'package:flutter_tools/src/commands/logs.dart';
-import 'package:mockito/mockito.dart';
+import 'package:flutter_tools/src/runner/flutter_command_runner.dart';
 import 'package:test/test.dart';
 
 import 'src/mocks.dart';
@@ -13,18 +13,13 @@
 
 defineTests() {
   group('logs', () {
-    test('returns 0 when no device is connected', () {
+    test('fail with a bad device id', () {
       LogsCommand command = new LogsCommand();
       applyMocksToCommand(command);
-      MockDeviceStore mockDevices = command.devices;
-
-      when(mockDevices.android.isConnected()).thenReturn(false);
-      when(mockDevices.iOS.isConnected()).thenReturn(false);
-      when(mockDevices.iOSSimulator.isConnected()).thenReturn(false);
-
-      CommandRunner runner = new CommandRunner('test_flutter', '')
-        ..addCommand(command);
-      runner.run(['logs']).then((int code) => expect(code, equals(0)));
+      CommandRunner runner = new FlutterCommandRunner()..addCommand(command);
+      runner.run(<String>['-d', 'abc123', 'logs']).then((int code) {
+        expect(code, equals(1));
+      });
     });
   });
 }
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index 9af0463..99161d6 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -48,5 +48,6 @@
   command
     ..applicationPackages = new MockApplicationPackageStore()
     ..toolchain = new MockToolchain()
-    ..devices = new MockDeviceStore();
+    ..devices = new MockDeviceStore()
+    ..projectRootValidator = () => true;
 }