add the ability to start and stop device polling
diff --git a/packages/flutter_tools/lib/src/android/adb.dart b/packages/flutter_tools/lib/src/android/adb.dart
index 2392dc3..377bca8 100644
--- a/packages/flutter_tools/lib/src/android/adb.dart
+++ b/packages/flutter_tools/lib/src/android/adb.dart
@@ -74,7 +74,7 @@
     ).toList();
   }
 
-  /// Listen to device activations and deactivations via the asb server's
+  /// Listen to device activations and deactivations via the adb server's
   /// 'track-devices' command. Call cancel on the returned stream to stop
   /// listening.
   Stream<List<AdbDevice>> trackDevices() {
diff --git a/packages/flutter_tools/lib/src/android/device_android.dart b/packages/flutter_tools/lib/src/android/android_device.dart
similarity index 98%
rename from packages/flutter_tools/lib/src/android/device_android.dart
rename to packages/flutter_tools/lib/src/android/android_device.dart
index 404c0d8..f909666 100644
--- a/packages/flutter_tools/lib/src/android/device_android.dart
+++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -29,17 +29,11 @@
 // Path where the snapshot will be copied on the device.
 const String _deviceSnapshotPath = '/data/local/tmp/dev_snapshot.bin';
 
-class AndroidDeviceDiscovery extends DeviceDiscovery {
-  List<Device> _devices = <Device>[];
+class AndroidDevices extends PollingDeviceDiscovery {
+  AndroidDevices() : super('AndroidDevices');
 
   bool get supportsPlatform => true;
-
-  Future init() {
-    _devices = getAdbDevices();
-    return new Future.value();
-  }
-
-  List<Device> get devices => _devices;
+  List<Device> pollingGetDevices() => getAdbDevices();
 }
 
 class AndroidDevice extends Device {
diff --git a/packages/flutter_tools/lib/src/base/utils.dart b/packages/flutter_tools/lib/src/base/utils.dart
new file mode 100644
index 0000000..b271fc1
--- /dev/null
+++ b/packages/flutter_tools/lib/src/base/utils.dart
@@ -0,0 +1,48 @@
+// 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';
+
+/// A class to maintain a list of items, fire events when items are added or
+/// removed, and calculate a diff of changes when a new list of items is
+/// available.
+class ItemListNotifier<T> {
+  ItemListNotifier() {
+    _items = new Set<T>();
+  }
+
+  ItemListNotifier.from(List<T> items) {
+    _items = new Set<T>.from(items);
+  }
+
+  Set<T> _items;
+
+  StreamController<T> _addedController = new StreamController<T>.broadcast();
+  StreamController<T> _removedController = new StreamController<T>.broadcast();
+
+  Stream<T> get onAdded => _addedController.stream;
+  Stream<T> get onRemoved => _removedController.stream;
+
+  List<T> get items => _items.toList();
+
+  void updateWithNewList(List<T> updatedList) {
+    Set<T> updatedSet = new Set<T>.from(updatedList);
+
+    Set<T> addedItems = updatedSet.difference(_items);
+    Set<T> removedItems = _items.difference(updatedSet);
+
+    _items = updatedSet;
+
+    for (T item in addedItems)
+      _addedController.add(item);
+    for (T item in removedItems)
+      _removedController.add(item);
+  }
+
+  /// Close the streams.
+  void dispose() {
+    _addedController.close();
+    _removedController.close();
+  }
+}
diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart
index 252e929..beea6cb 100644
--- a/packages/flutter_tools/lib/src/commands/daemon.dart
+++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -6,15 +6,13 @@
 import 'dart:convert';
 import 'dart:io';
 
-import '../android/adb.dart';
-import '../android/android_sdk.dart';
-import '../android/device_android.dart';
+import '../android/android_device.dart';
 import '../base/context.dart';
 import '../base/logger.dart';
 import '../device.dart';
 import '../globals.dart';
-import '../ios/device_ios.dart';
-import '../ios/simulator.dart';
+import '../ios/devices.dart';
+import '../ios/simulators.dart';
 import '../runner/flutter_command.dart';
 import 'run.dart';
 import 'stop.dart' as stop;
@@ -126,10 +124,8 @@
         throw 'no domain for method: $method';
 
       _domainMap[prefix].handleCommand(name, id, request['params']);
-    } catch (error, trace) {
+    } catch (error) {
       _send({'id': id, 'error': _toJsonable(error)});
-      stderr.writeln('error handling $request: $error');
-      stderr.writeln(trace);
     }
   }
 
@@ -170,8 +166,6 @@
       }
     }).catchError((error, trace) {
       _send({'id': id, 'error': _toJsonable(error)});
-      stderr.writeln("error handling '$name.$command': $error");
-      stderr.writeln(trace);
     });
   }
 
@@ -286,166 +280,65 @@
 
 /// This domain lets callers list and monitor connected devices.
 ///
-/// It exports a `getDevices()` call, as well as firing `device.added`,
-/// `device.removed`, and `device.changed` events.
+/// It exports a `getDevices()` call, as well as firing `device.added` and
+/// `device.removed` events.
 class DeviceDomain extends Domain {
   DeviceDomain(Daemon daemon) : super(daemon, 'device') {
     registerHandler('getDevices', getDevices);
+    registerHandler('enable', enable);
+    registerHandler('disable', disable);
 
-    _androidDeviceDiscovery = new AndroidDeviceDiscovery();
-    _androidDeviceDiscovery.onAdded.listen((Device device) {
-      sendEvent('device.added', _deviceToMap(device));
-    });
-    _androidDeviceDiscovery.onRemoved.listen((Device device) {
-      sendEvent('device.removed', _deviceToMap(device));
-    });
-    _androidDeviceDiscovery.onChanged.listen((Device device) {
-      sendEvent('device.changed', _deviceToMap(device));
-    });
+    PollingDeviceDiscovery deviceDiscovery = new AndroidDevices();
+    if (deviceDiscovery.supportsPlatform)
+      _discoverers.add(deviceDiscovery);
 
-    if (Platform.isMacOS) {
-      _iosSimulatorDeviceDiscovery = new IOSSimulatorDeviceDiscovery();
-      _iosSimulatorDeviceDiscovery.onAdded.listen((Device device) {
+    deviceDiscovery = new IOSDevices();
+    if (deviceDiscovery.supportsPlatform)
+      _discoverers.add(deviceDiscovery);
+
+    deviceDiscovery = new IOSSimulators();
+    if (deviceDiscovery.supportsPlatform)
+      _discoverers.add(deviceDiscovery);
+
+    for (PollingDeviceDiscovery discoverer in _discoverers) {
+      discoverer.onAdded.listen((Device device) {
         sendEvent('device.added', _deviceToMap(device));
       });
-      _iosSimulatorDeviceDiscovery.onRemoved.listen((Device device) {
+      discoverer.onRemoved.listen((Device device) {
         sendEvent('device.removed', _deviceToMap(device));
       });
     }
   }
 
-  AndroidDeviceDiscovery _androidDeviceDiscovery;
-  IOSSimulatorDeviceDiscovery _iosSimulatorDeviceDiscovery;
+  List<PollingDeviceDiscovery> _discoverers = <PollingDeviceDiscovery>[];
 
   Future<List<Device>> getDevices(dynamic args) {
-    List<Device> devices = <Device>[];
-    devices.addAll(_androidDeviceDiscovery.getDevices());
-    if (_iosSimulatorDeviceDiscovery != null)
-      devices.addAll(_iosSimulatorDeviceDiscovery.getDevices());
+    List<Device> devices = _discoverers.expand((PollingDeviceDiscovery discoverer) {
+      return discoverer.devices;
+    }).toList();
     return new Future.value(devices);
   }
 
-  void dispose() {
-    _androidDeviceDiscovery.dispose();
-    _iosSimulatorDeviceDiscovery?.dispose();
-  }
-}
-
-class AndroidDeviceDiscovery {
-  AndroidDeviceDiscovery() {
-    _initAdb();
-
-    if (_adb != null) {
-      _subscription = _adb.trackDevices().listen(_handleUpdatedDevices);
+  /// Enable device events.
+  Future enable(dynamic args) {
+    for (PollingDeviceDiscovery discoverer in _discoverers) {
+      discoverer.startPolling();
     }
+    return new Future.value();
   }
 
-  Adb _adb;
-  StreamSubscription _subscription;
-  Map<String, AndroidDevice> _devices = new Map<String, AndroidDevice>();
-
-  StreamController<Device> addedController = new StreamController<Device>.broadcast();
-  StreamController<Device> removedController = new StreamController<Device>.broadcast();
-  StreamController<Device> changedController = new StreamController<Device>.broadcast();
-
-  List<Device> getDevices() => _devices.values.toList();
-
-  Stream<Device> get onAdded => addedController.stream;
-  Stream<Device> get onRemoved => removedController.stream;
-  Stream<Device> get onChanged => changedController.stream;
-
-  void _initAdb() {
-    if (_adb == null) {
-      _adb = new Adb(getAdbPath(androidSdk));
-      if (!_adb.exists())
-        _adb = null;
+  /// Disable device events.
+  Future disable(dynamic args) {
+    for (PollingDeviceDiscovery discoverer in _discoverers) {
+      discoverer.stopPolling();
     }
-  }
-
-  void _handleUpdatedDevices(List<AdbDevice> newDevices) {
-    List<AndroidDevice> currentDevices = new List.from(getDevices());
-
-    for (AdbDevice device in newDevices) {
-      AndroidDevice androidDevice = _devices[device.id];
-
-      if (androidDevice == null) {
-        // device added
-        androidDevice = new AndroidDevice(
-          device.id,
-          productID: device.productID,
-          modelID: device.modelID,
-          deviceCodeName: device.deviceCodeName,
-          connected: device.isAvailable
-        );
-        _devices[androidDevice.id] = androidDevice;
-        addedController.add(androidDevice);
-      } else {
-        currentDevices.remove(androidDevice);
-
-        // check state
-        if (androidDevice.isConnected() != device.isAvailable) {
-          androidDevice.setConnected(device.isAvailable);
-          changedController.add(androidDevice);
-        }
-      }
-    }
-
-    // device removed
-    for (AndroidDevice device in currentDevices) {
-      _devices.remove(device.id);
-
-      removedController.add(device);
-    }
+    return new Future.value();
   }
 
   void dispose() {
-    _subscription?.cancel();
-  }
-}
-
-class IOSSimulatorDeviceDiscovery {
-  IOSSimulatorDeviceDiscovery() {
-    _subscription = SimControl.trackDevices().listen(_handleUpdatedDevices);
-  }
-
-  StreamSubscription<List<SimDevice>> _subscription;
-
-  Map<String, IOSSimulator> _devices = new Map<String, IOSSimulator>();
-
-  StreamController<Device> addedController = new StreamController<Device>.broadcast();
-  StreamController<Device> removedController = new StreamController<Device>.broadcast();
-
-  List<Device> getDevices() => _devices.values.toList();
-
-  Stream<Device> get onAdded => addedController.stream;
-  Stream<Device> get onRemoved => removedController.stream;
-
-  void _handleUpdatedDevices(List<SimDevice> newDevices) {
-    List<IOSSimulator> currentDevices = new List.from(getDevices());
-
-    for (SimDevice device in newDevices) {
-      IOSSimulator androidDevice = _devices[device.udid];
-
-      if (androidDevice == null) {
-        // device added
-        androidDevice = new IOSSimulator(device.udid, name: device.name);
-        _devices[androidDevice.id] = androidDevice;
-        addedController.add(androidDevice);
-      } else {
-        currentDevices.remove(androidDevice);
-      }
+    for (PollingDeviceDiscovery discoverer in _discoverers) {
+      discoverer.dispose();
     }
-
-    // device removed
-    for (IOSSimulator device in currentDevices) {
-      _devices.remove(device.id);
-
-      removedController.add(device);
-    }
-  }
-
-  void dispose() {
-    _subscription?.cancel();
   }
 }
 
@@ -490,7 +383,7 @@
   }
 
   void printTrace(String message) {
-    _messageController.add(new LogMessage('trace', message));
+    // This is a lot of traffic to send over the wire.
   }
 }
 
diff --git a/packages/flutter_tools/lib/src/commands/install.dart b/packages/flutter_tools/lib/src/commands/install.dart
index b9f8684..8e17360 100644
--- a/packages/flutter_tools/lib/src/commands/install.dart
+++ b/packages/flutter_tools/lib/src/commands/install.dart
@@ -7,7 +7,7 @@
 
 import '../application_package.dart';
 import '../device.dart';
-import '../ios/simulator.dart';
+import '../ios/simulators.dart';
 import '../runner/flutter_command.dart';
 
 class InstallCommand extends FlutterCommand {
diff --git a/packages/flutter_tools/lib/src/commands/trace.dart b/packages/flutter_tools/lib/src/commands/trace.dart
index a5b0744..f8f7a43 100644
--- a/packages/flutter_tools/lib/src/commands/trace.dart
+++ b/packages/flutter_tools/lib/src/commands/trace.dart
@@ -4,7 +4,7 @@
 
 import 'dart:async';
 
-import '../android/device_android.dart';
+import '../android/android_device.dart';
 import '../application_package.dart';
 import '../globals.dart';
 import '../runner/flutter_command.dart';
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index 04562ad..df00d2b 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -4,12 +4,14 @@
 
 import 'dart:async';
 
-import 'android/device_android.dart';
+import 'android/android_device.dart';
 import 'application_package.dart';
 import 'base/common.dart';
+import 'base/utils.dart';
 import 'build_configuration.dart';
 import 'globals.dart';
-import 'ios/device_ios.dart';
+import 'ios/devices.dart';
+import 'ios/simulators.dart';
 import 'toolchain.dart';
 
 /// A class to get all available devices.
@@ -18,27 +20,9 @@
   /// of their methods are invoked.
   DeviceManager() {
     // Register the known discoverers.
-    _deviceDiscoverers.add(new AndroidDeviceDiscovery());
-    _deviceDiscoverers.add(new IOSDeviceDiscovery());
-    _deviceDiscoverers.add(new IOSSimulatorDiscovery());
-  }
-
-  Future _init() {
-    if (_initedCompleter == null) {
-      _initedCompleter = new Completer();
-
-      Future.forEach(_deviceDiscoverers, (DeviceDiscovery discoverer) {
-        if (!discoverer.supportsPlatform)
-          return null;
-        return discoverer.init();
-      }).then((_) {
-        _initedCompleter.complete();
-      }).catchError((error, stackTrace) {
-        _initedCompleter.completeError(error, stackTrace);
-      });
-    }
-
-    return _initedCompleter.future;
+    _deviceDiscoverers.add(new AndroidDevices());
+    _deviceDiscoverers.add(new IOSDevices());
+    _deviceDiscoverers.add(new IOSSimulators());
   }
 
   List<DeviceDiscovery> _deviceDiscoverers = <DeviceDiscovery>[];
@@ -46,8 +30,6 @@
   /// A user-specified device ID.
   String specifiedDeviceId;
 
-  Completer _initedCompleter;
-
   bool get hasSpecifiedDeviceId => specifiedDeviceId != null;
 
   /// Return the device with the matching ID; else, complete the Future with
@@ -75,8 +57,6 @@
 
   /// Return the list of all connected devices.
   Future<List<Device>> getAllConnectedDevices() async {
-    await _init();
-
     return _deviceDiscoverers
       .where((DeviceDiscovery discoverer) => discoverer.supportsPlatform)
       .expand((DeviceDiscovery discoverer) => discoverer.devices)
@@ -87,10 +67,60 @@
 /// An abstract class to discover and enumerate a specific type of devices.
 abstract class DeviceDiscovery {
   bool get supportsPlatform;
-  Future init();
   List<Device> get devices;
 }
 
+/// A [DeviceDiscovery] implementation that uses polling to discover device adds
+/// and removals.
+abstract class PollingDeviceDiscovery extends DeviceDiscovery {
+  PollingDeviceDiscovery(this.name);
+
+  static const Duration _pollingDuration = const Duration(seconds: 4);
+
+  final String name;
+  ItemListNotifier<Device> _items;
+  Timer _timer;
+
+  List<Device> pollingGetDevices();
+
+  void startPolling() {
+    if (_timer == null) {
+      if (_items == null)
+        _items = new ItemListNotifier<Device>();
+      _timer = new Timer.periodic(_pollingDuration, (Timer timer) {
+        _items.updateWithNewList(pollingGetDevices());
+      });
+    }
+  }
+
+  void stopPolling() {
+    _timer?.cancel();
+    _timer = null;
+  }
+
+  List<Device> get devices {
+    if (_items == null)
+      _items = new ItemListNotifier<Device>.from(pollingGetDevices());
+    return _items.items;
+  }
+
+  Stream<Device> get onAdded {
+    if (_items == null)
+      _items = new ItemListNotifier<Device>();
+    return _items.onAdded;
+  }
+
+  Stream<Device> get onRemoved {
+    if (_items == null)
+      _items = new ItemListNotifier<Device>();
+    return _items.onRemoved;
+  }
+
+  void dispose() => stopPolling();
+
+  String toString() => '$name device discovery';
+}
+
 abstract class Device {
   Device(this.id);
 
@@ -139,6 +169,16 @@
   /// Stop an app package on the current device.
   Future<bool> stopApp(ApplicationPackage app);
 
+  int get hashCode => id.hashCode;
+
+  bool operator ==(dynamic other) {
+    if (identical(this, other))
+      return true;
+    if (other is! Device)
+      return false;
+    return id == other.id;
+  }
+
   String toString() => '$runtimeType $id';
 }
 
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;
+  }
+}
diff --git a/packages/flutter_tools/test/android_device_test.dart b/packages/flutter_tools/test/android_device_test.dart
index d71878d..2ad4701 100644
--- a/packages/flutter_tools/test/android_device_test.dart
+++ b/packages/flutter_tools/test/android_device_test.dart
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:flutter_tools/src/android/device_android.dart';
+import 'package:flutter_tools/src/android/android_device.dart';
 import 'package:test/test.dart';
 
 import 'src/context.dart';
diff --git a/packages/flutter_tools/test/base_utils_test.dart b/packages/flutter_tools/test/base_utils_test.dart
new file mode 100644
index 0000000..053fdd4
--- /dev/null
+++ b/packages/flutter_tools/test/base_utils_test.dart
@@ -0,0 +1,37 @@
+// 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 'package:flutter_tools/src/base/utils.dart';
+import 'package:test/test.dart';
+
+main() => defineTests();
+
+defineTests() {
+  group('ItemListNotifier', () {
+    test('sends notifications', () async {
+      ItemListNotifier<String> list = new ItemListNotifier<String>();
+      expect(list.items, isEmpty);
+
+      Future<List<String>> addedStreamItems = list.onAdded.toList();
+      Future<List<String>> removedStreamItems = list.onRemoved.toList();
+
+      list.updateWithNewList(<String>['aaa']);
+      list.updateWithNewList(<String>['aaa', 'bbb']);
+      list.updateWithNewList(<String>['bbb']);
+      list.dispose();
+
+      List<String> addedItems = await addedStreamItems;
+      List<String> removedItems = await removedStreamItems;
+
+      expect(addedItems.length, 2);
+      expect(addedItems.first, 'aaa');
+      expect(addedItems[1], 'bbb');
+
+      expect(removedItems.length, 1);
+      expect(removedItems.first, 'aaa');
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/daemon_test.dart b/packages/flutter_tools/test/daemon_test.dart
index 70035a9..29ff62f 100644
--- a/packages/flutter_tools/test/daemon_test.dart
+++ b/packages/flutter_tools/test/daemon_test.dart
@@ -3,11 +3,14 @@
 // found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:io';
 
 import 'package:flutter_tools/src/base/context.dart';
 import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/commands/daemon.dart';
+import 'package:flutter_tools/src/doctor.dart';
 import 'package:flutter_tools/src/globals.dart';
+import 'package:flutter_tools/src/ios/mac.dart';
 import 'package:mockito/mockito.dart';
 import 'package:test/test.dart';
 
@@ -31,6 +34,9 @@
       appContext = new AppContext();
       notifyingLogger = new NotifyingLogger();
       appContext[Logger] = notifyingLogger;
+      appContext[Doctor] = new Doctor();
+      if (Platform.isMacOS)
+        appContext[XCode] = new XCode();
     });
 
     tearDown(() {
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index a5f3ee3..8936cf9 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -2,11 +2,12 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:flutter_tools/src/android/device_android.dart';
+import 'package:flutter_tools/src/android/android_device.dart';
 import 'package:flutter_tools/src/application_package.dart';
 import 'package:flutter_tools/src/build_configuration.dart';
 import 'package:flutter_tools/src/device.dart';
-import 'package:flutter_tools/src/ios/device_ios.dart';
+import 'package:flutter_tools/src/ios/devices.dart';
+import 'package:flutter_tools/src/ios/simulators.dart';
 import 'package:flutter_tools/src/runner/flutter_command.dart';
 import 'package:flutter_tools/src/toolchain.dart';
 import 'package:mockito/mockito.dart';