[fuchsia_reload] Reload each instance of a module. (#9379)

diff --git a/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart b/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart
index e399fc6..37ca15d 100644
--- a/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart
+++ b/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart
@@ -40,15 +40,24 @@
       help: 'Path to Fuchsia source tree.');
     argParser.addOption('gn-target',
       abbr: 'g',
-      help: 'GN target of the application, e.g //path/to/app:app');
+      help: 'GN target of the application, e.g //path/to/app:app.');
+    argParser.addFlag('list',
+      abbr: 'l',
+      defaultsTo: false,
+      help: 'Lists the running modules. '
+            'Requires the flags --address(-a) and --fuchsia-root(-f).');
     argParser.addOption('name-override',
       abbr: 'n',
       help: 'On-device name of the application binary.');
+    argParser.addOption('isolate-number',
+      abbr: 'i',
+      help: 'To reload only one instance, speficy the isolate number, e.g. '
+            'the number in foo\$main-###### given by --list.');
     argParser.addOption('target',
       abbr: 't',
       defaultsTo: flx.defaultMainPath,
       help: 'Target app path / main entry-point file. '
-            'Relative to --gn-target path, e.g. lib/main.dart');
+            'Relative to --gn-target path, e.g. lib/main.dart.');
   }
 
   @override
@@ -61,11 +70,14 @@
   String _projectRoot;
   String _projectName;
   String _binaryName;
+  String _isolateNumber;
   String _fuchsiaProjectPath;
   String _target;
   String _address;
   String _dotPackagesPath;
 
+  bool _list;
+
   @override
   Future<Null> runCommand() async {
     Cache.releaseLockEarly();
@@ -79,9 +91,14 @@
     for (int port in servicePorts)
       printTrace('Fuchsia service port: $port');
 
+    if (_list) {
+      await _listViews(servicePorts);
+      return;
+    }
+
     // Check that there are running VM services on the returned
     // ports, and find the Isolates that are running the target app.
-    final String isolateName = '$_binaryName\$main';
+    final String isolateName = '$_binaryName\$main$_isolateNumber';
     final List<int> targetPorts = await _filterPorts(servicePorts, isolateName);
     if (targetPorts.isEmpty)
       throwToolExit('No VMs found running $_binaryName.');
@@ -90,43 +107,55 @@
 
     // Set up a device and hot runner and attach the hot runner to the first
     // vm service we found.
-    final int firstPort = targetPorts[0];
-    final String fullAddress = '$_address:$firstPort';
-    final FuchsiaDevice device = new FuchsiaDevice(fullAddress);
+    final List<String> fullAddresses = targetPorts.map(
+      (int p) => '$_address:$p'
+    ).toList();
+    final FuchsiaDevice device = new FuchsiaDevice(fullAddresses[0]);
     final HotRunner hotRunner = new HotRunner(
       device,
       debuggingOptions: new DebuggingOptions.enabled(getBuildMode()),
       target: _target,
       projectRootPath: _fuchsiaProjectPath,
-      packagesFilePath: _dotPackagesPath,
+      packagesFilePath: _dotPackagesPath
     );
-    final Uri observatoryUri = Uri.parse('http://$fullAddress');
-    printStatus('Connecting to $_binaryName at $observatoryUri');
-    await hotRunner.attach(observatoryUri, isolateFilter: isolateName);
+    final List<Uri> observatoryUris =
+      fullAddresses.map((String a) => Uri.parse('http://$a')).toList();
+    printStatus('Connecting to $_binaryName');
+    await hotRunner.attach(observatoryUris, isolateFilter: isolateName);
   }
 
-  // Find ports where there is a view isolate with the given name
-  Future<List<int>> _filterPorts(List<int> ports, String isolateFilter) async {
-    final List<int> result = <int>[];
+  Future<List<FlutterView>> _getViews(List<int> ports) async {
+    final List<FlutterView> views = <FlutterView>[];
     for (int port in ports) {
       final String addr = 'http://$_address:$port';
       final Uri uri = Uri.parse(addr);
       final VMService vmService = VMService.connect(uri);
       await vmService.getVM();
       await vmService.waitForViews();
-      if (vmService.vm.firstView == null) {
-        printTrace('Found no views at $addr');
-        continue;
-      }
-      for (FlutterView v in vmService.vm.views) {
-        printTrace('At $addr, found view: ${v.uiIsolate.name}');
-        if (v.uiIsolate.name.indexOf(isolateFilter) == 0)
-          result.add(port);
-      }
+      views.addAll(vmService.vm.views);
+    }
+    return views;
+  }
+
+  // Find ports where there is a view isolate with the given name
+  Future<List<int>> _filterPorts(List<int> ports, String isolateFilter) async {
+    final List<int> result = <int>[];
+    for (FlutterView v in await _getViews(ports)) {
+      final Uri addr = v.owner.vmService.httpAddress;
+      printTrace('At $addr, found view: ${v.uiIsolate.name}');
+      if (v.uiIsolate.name.indexOf(isolateFilter) == 0)
+        result.add(addr.port);
     }
     return result;
   }
 
+  Future<Null> _listViews(List<int> ports) async {
+    for (FlutterView v in await _getViews(ports)) {
+      final Uri addr = v.owner.vmService.httpAddress;
+      printStatus('At $addr, found view: ${v.uiIsolate.name}');
+    }
+  }
+
   void _validateArguments() {
     _fuchsiaRoot = argResults['fuchsia-root'];
     if (_fuchsiaRoot == null)
@@ -138,6 +167,12 @@
     if (_address == null)
       throwToolExit('Give the address of the device running Fuchsia with --address.');
 
+    _list = argResults['list'];
+    if (_list) {
+      // For --list, we only need the device address and the Fuchsia tree root.
+      return;
+    }
+
     final List<String> gnTarget = _extractPathAndName(argResults['gn-target']);
     _projectRoot = gnTarget[0];
     _projectName = gnTarget[1];
@@ -166,6 +201,13 @@
     } else {
       _binaryName = nameOverride;
     }
+
+    final String isolateNumber = argResults['isolate-number'];
+    if (isolateNumber == null) {
+      _isolateNumber = '';
+    } else {
+      _isolateNumber = '-$isolateNumber';
+    }
   }
 
   List<String> _extractPathAndName(String gnTarget) {
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index d843fee..2cef785 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -64,8 +64,7 @@
   bool get isRunningRelease => debuggingOptions.buildMode == BuildMode.release;
   bool get supportsServiceProtocol => isRunningDebug || isRunningProfile;
 
-  VMService vmService;
-  FlutterView currentView;
+  List<VMService> vmServices;
   StreamSubscription<String> _loggingSubscription;
 
   /// Start the app and keep the process running during its lifetime.
@@ -78,6 +77,25 @@
 
   bool get supportsRestart => false;
 
+  String isolateFilter;
+
+  List<FlutterView> _currentViewsCache;
+  List<FlutterView> get currentViews {
+    if (_currentViewsCache == null) {
+      if ((vmServices == null) || vmServices.isEmpty)
+        return null;
+      if (isolateFilter == null)
+        return vmServices[0].vm.views.toList();
+      final List<FlutterView> result = <FlutterView>[];
+      for (VMService service in vmServices)
+        result.addAll(service.vm.allViewsWithName(isolateFilter));
+      _currentViewsCache = result;
+    }
+    return _currentViewsCache;
+  }
+
+  FlutterView get currentView => (currentViews != null) ? currentViews[0] : null;
+
   Future<OperationResult> restart({ bool fullRestart: false, bool pauseAfterRestart: false }) {
     throw 'unsupported';
   }
@@ -94,21 +112,26 @@
     appFinished();
   }
 
+  Future<Null> refreshViews() async {
+    if ((vmServices == null) || vmServices.isEmpty)
+      return;
+    for (VMService service in vmServices)
+      await service.vm.refreshViews();
+    _currentViewsCache = null;
+  }
+
   Future<Null> _debugDumpApp() async {
-    if (vmService != null)
-      await vmService.vm.refreshViews();
+    await refreshViews();
     await currentView.uiIsolate.flutterDebugDumpApp();
   }
 
   Future<Null> _debugDumpRenderTree() async {
-    if (vmService != null)
-      await vmService.vm.refreshViews();
+    await refreshViews();
     await currentView.uiIsolate.flutterDebugDumpRenderTree();
   }
 
   Future<Null> _debugToggleDebugPaintSizeEnabled() async {
-    if (vmService != null)
-      await vmService.vm.refreshViews();
+    await refreshViews();
     await currentView.uiIsolate.flutterToggleDebugPaintSizeEnabled();
   }
 
@@ -117,8 +140,7 @@
     final File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png');
     try {
       if (supportsServiceProtocol && isRunningDebug) {
-        if (vmService != null)
-          await vmService.vm.refreshViews();
+        await refreshViews();
         try {
           await currentView.uiIsolate.flutterDebugAllowBanner(false);
         } catch (error) {
@@ -148,8 +170,7 @@
   }
 
   Future<String> _debugRotatePlatform() async {
-    if (vmService != null)
-      await vmService.vm.refreshViews();
+    await refreshViews();
     switch (await currentView.uiIsolate.flutterPlatformOverride()) {
       case 'iOS':
         return await currentView.uiIsolate.flutterPlatformOverride('android');
@@ -209,25 +230,37 @@
     _loggingSubscription = null;
   }
 
-  Future<Null> connectToServiceProtocol(Uri uri, {String isolateFilter}) async {
-    if (!debuggingOptions.debuggingEnabled)
+  Future<Null> connectToServiceProtocol(
+    List<Uri> uris, {
+    String isolateFilter
+  }) async {
+    if (!debuggingOptions.debuggingEnabled) {
       return new Future<Null>.error('Error the service protocol is not enabled.');
-    vmService = VMService.connect(uri);
-    printTrace('Connected to service protocol: $uri');
-    await vmService.getVM();
+    }
+    final List<VMService> services = new List<VMService>(uris.length);
+    for (int i = 0; i < uris.length; i++) {
+      services[i] = VMService.connect(uris[i]);
+      printTrace('Connected to service protocol: ${uris[i]}');
+    }
+    vmServices = services;
+    for (VMService service in services)
+      await service.getVM();
+
+    this.isolateFilter = isolateFilter;
 
     // Refresh the view list, and wait a bit for the list to populate.
-    await vmService.waitForViews();
-    currentView = (isolateFilter == null)
-                ? vmService.vm.firstView
-                : vmService.vm.firstViewWithName(isolateFilter);
+    for (VMService service in services)
+      await service.waitForViews();
     if (currentView == null)
       throwToolExit('No Flutter view is available');
 
     // Listen for service protocol connection to close.
-    vmService.done.then<Null>(
+    for (VMService service in services) {
+      service.done.then<Null>(
         _serviceProtocolDone,
-        onError: _serviceProtocolError).whenComplete(appFinished);
+        onError: _serviceProtocolError
+      ).whenComplete(appFinished);
+    }
   }
 
   Future<Null> _serviceProtocolDone(dynamic object) {
@@ -357,7 +390,10 @@
   Future<Null> preStop() async { }
 
   Future<Null> stopApp() async {
-    if (vmService != null && !vmService.isClosed) {
+    if (vmServices != null &&
+        vmServices.isNotEmpty &&
+        !vmServices[0].isClosed) {
+      // TODO(zra): iterate over all the views.
       if ((currentView != null) && (currentView.uiIsolate != null)) {
         // TODO(johnmccutchan): Wait for the exit command to complete.
         currentView.uiIsolate.flutterExit();
diff --git a/packages/flutter_tools/lib/src/run_cold.dart b/packages/flutter_tools/lib/src/run_cold.dart
index a31a326..4db138c 100644
--- a/packages/flutter_tools/lib/src/run_cold.dart
+++ b/packages/flutter_tools/lib/src/run_cold.dart
@@ -101,27 +101,27 @@
 
     // Connect to observatory.
     if (debuggingOptions.debuggingEnabled)
-      await connectToServiceProtocol(_result.observatoryUri);
+      await connectToServiceProtocol(<Uri>[_result.observatoryUri]);
 
     if (_result.hasObservatory) {
       connectionInfoCompleter?.complete(new DebugConnectionInfo(
         httpUri: _result.observatoryUri,
-        wsUri: vmService.wsAddress,
+        wsUri: vmServices[0].wsAddress,
       ));
     }
 
     printTrace('Application running.');
 
-    if (vmService != null) {
-      device.getLogReader(app: package).appPid = vmService.vm.pid;
-      await vmService.vm.refreshViews();
-      printTrace('Connected to ${vmService.vm.firstView}\.');
+    if (vmServices != null && vmServices.isNotEmpty) {
+      device.getLogReader(app: package).appPid = vmServices[0].vm.pid;
+      await refreshViews();
+      printTrace('Connected to $currentView.');
     }
 
-    if (vmService != null && traceStartup) {
+    if (vmServices != null && vmServices.isNotEmpty && traceStartup) {
       printStatus('Downloading startup trace info...');
       try {
-        await downloadStartupTrace(vmService);
+        await downloadStartupTrace(vmServices[0]);
       } catch(error) {
         printError(error);
         return 2;
@@ -174,7 +174,7 @@
   @override
   Future<Null> preStop() async {
     // If we're running in release mode, stop the app using the device logic.
-    if (vmService == null)
+    if (vmServices == null || vmServices.isEmpty)
       await device.stopApp(package);
   }
 }
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index 61d1bce..77b1045 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -87,21 +87,21 @@
     return true;
   }
 
-  Future<int> attach(Uri observatoryUri, {
+  Future<int> attach(List<Uri> observatoryUris, {
     Completer<DebugConnectionInfo> connectionInfoCompleter,
     Completer<Null> appStartedCompleter,
     String isolateFilter,
   }) async {
-    _observatoryUri = observatoryUri;
+    _observatoryUri = observatoryUris[0];
     try {
       await connectToServiceProtocol(
-          _observatoryUri, isolateFilter: isolateFilter);
+          observatoryUris, isolateFilter: isolateFilter);
     } catch (error) {
       printError('Error connecting to the service protocol: $error');
       return 2;
     }
 
-    device.getLogReader(app: package).appPid = vmService.vm.pid;
+    device.getLogReader(app: package).appPid = vmServices[0].vm.pid;
 
     try {
       final Uri baseUri = await _initDevFS();
@@ -109,7 +109,7 @@
         connectionInfoCompleter.complete(
           new DebugConnectionInfo(
             httpUri: _observatoryUri,
-            wsUri: vmService.wsAddress,
+            wsUri: vmServices[0].wsAddress,
             baseUri: baseUri.toString()
           )
         );
@@ -124,8 +124,9 @@
       return 3;
     }
 
-    await vmService.vm.refreshViews();
-    printTrace('Connected to $currentView.');
+    await refreshViews();
+    for (FlutterView view in currentViews)
+      printTrace('Connected to $view.');
 
     if (stayResident) {
       setupTerminal();
@@ -140,7 +141,7 @@
       // Measure time to perform a hot restart.
       printStatus('Benchmarking hot restart');
       await restart(fullRestart: true);
-      await vmService.vm.refreshViews();
+      await refreshViews();
       // TODO(johnmccutchan): Modify script entry point.
       printStatus('Benchmarking hot reload');
       // Measure time to perform a hot reload.
@@ -219,7 +220,7 @@
       return 2;
     }
 
-    return attach(result.observatoryUri,
+    return attach(<Uri>[result.observatoryUri],
                   connectionInfoCompleter: connectionInfoCompleter,
                   appStartedCompleter: appStartedCompleter);
   }
@@ -241,7 +242,7 @@
 
   Future<Uri> _initDevFS() {
     final String fsName = fs.path.basename(projectRootPath);
-    _devFS = new DevFS(vmService,
+    _devFS = new DevFS(vmServices[0],
                        fsName,
                        fs.directory(projectRootPath),
                        packagesFilePath: packagesFilePath);
@@ -425,11 +426,18 @@
       final Uri devicePackagesUri = _devFS.baseUri.resolve('.packages');
       if (benchmarkMode)
         vmReloadTimer.start();
-      final Map<String, dynamic> reloadReport =
-          await currentView.uiIsolate.reloadSources(
-              pause: pause,
-              rootLibUri: deviceEntryUri,
-              packagesUri: devicePackagesUri);
+
+      Map<String, dynamic> reloadReport;
+      for (FlutterView view in currentViews) {
+        final Map<String, dynamic> report = await view.uiIsolate.reloadSources(
+          pause: pause,
+          rootLibUri: deviceEntryUri,
+          packagesUri: devicePackagesUri
+        );
+        // Just take the first one until we think of something smart to do.
+        if (reloadReport == null)
+          reloadReport = report;
+      }
       if (!validateReloadReport(reloadReport)) {
         // Reload failed.
         flutterUsage.sendEvent('hot', 'reload-reject');
@@ -464,29 +472,43 @@
     if (benchmarkMode)
       reassembleTimer.start();
     // Reload the isolate.
-    await currentView.uiIsolate.reload();
+    for (FlutterView view in currentViews)
+      await view.uiIsolate.reload();
     // We are now running from source.
     _runningFromSnapshot = false;
     // Check if the isolate is paused.
-    final ServiceEvent pauseEvent = currentView.uiIsolate.pauseEvent;
-    if ((pauseEvent != null) && (pauseEvent.isPauseEvent)) {
-      // Isolate is paused. Stop here.
-      printTrace('Skipping reassemble because isolate is paused.');
+
+    final List<FlutterView> reassembleViews = <FlutterView>[];
+    for (FlutterView view in currentViews) {
+      final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
+      if ((pauseEvent != null) && (pauseEvent.isPauseEvent)) {
+        // Isolate is paused. Don't reassemble.
+        continue;
+      }
+      reassembleViews.add(view);
+    }
+    if (reassembleViews.isEmpty) {
+      printTrace('Skipping reassemble because all isolates are paused.');
       return new OperationResult(OperationResult.ok.code, reloadMessage);
     }
     await _evictDirtyAssets();
     printTrace('Reassembling application');
-    try {
-      await currentView.uiIsolate.flutterReassemble();
-    } catch (_) {
-      printError('Reassembling application failed.');
-      return new OperationResult(1, 'error reassembling application');
-    }
-    try {
-      /* ensure that a frame is scheduled */
-      await currentView.uiIsolate.uiWindowScheduleFrame();
-    } catch (_) {
-      /* ignore any errors */
+    bool reassembleAndScheduleErrors = false;
+    for (FlutterView view in reassembleViews) {
+      try {
+        await view.uiIsolate.flutterReassemble();
+      } catch (error) {
+        reassembleAndScheduleErrors = true;
+        printError('Reassembling ${view.uiIsolate.name} failed: $error');
+        continue;
+      }
+      try {
+        /* ensure that a frame is scheduled */
+        await view.uiIsolate.uiWindowScheduleFrame();
+      } catch (error) {
+        reassembleAndScheduleErrors = true;
+        printError('Scheduling a frame for ${view.uiIsolate.name} failed: $error');
+      }
     }
     reloadTimer.stop();
     printTrace('Hot reload performed in '
@@ -503,7 +525,10 @@
     }
     if (shouldReportReloadTime)
       flutterUsage.sendTiming('hot', 'reload', reloadTimer.elapsed);
-    return new OperationResult(OperationResult.ok.code, reloadMessage);
+    return new OperationResult(
+      reassembleAndScheduleErrors ? 1 : OperationResult.ok.code,
+      reloadMessage
+    );
   }
 
   @override
diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart
index ae7748b..9387d91 100644
--- a/packages/flutter_tools/lib/src/vmservice.dart
+++ b/packages/flutter_tools/lib/src/vmservice.dart
@@ -764,13 +764,12 @@
     return _viewCache.values.isEmpty ? null : _viewCache.values.first;
   }
 
-  FlutterView firstViewWithName(String isolateFilter) {
-    if (_viewCache.values.isEmpty) {
+  List<FlutterView> allViewsWithName(String isolateFilter) {
+    if (_viewCache.values.isEmpty)
       return null;
-    }
-    return _viewCache.values.firstWhere(
-        (FlutterView v) => v.uiIsolate.name.contains(isolateFilter),
-        orElse: () => null);
+    return _viewCache.values.where(
+      (FlutterView v) => v.uiIsolate.name.contains(isolateFilter)
+    ).toList();
   }
 }