Add a real-er web restart, doctor, workflow (#33786)

diff --git a/dev/devicelab/bin/tasks/linux_chrome_dev_mode.dart b/dev/devicelab/bin/tasks/linux_chrome_dev_mode.dart
new file mode 100644
index 0000000..a11b0ab
--- /dev/null
+++ b/dev/devicelab/bin/tasks/linux_chrome_dev_mode.dart
@@ -0,0 +1,10 @@
+// Copyright 2019 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 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/tasks/web_dev_mode_tests.dart';
+
+Future<void> main() async {
+  await task(createWebDevModeTest());
+}
diff --git a/dev/devicelab/bin/tasks/macos_chrome_dev_mode.dart b/dev/devicelab/bin/tasks/macos_chrome_dev_mode.dart
new file mode 100644
index 0000000..a11b0ab
--- /dev/null
+++ b/dev/devicelab/bin/tasks/macos_chrome_dev_mode.dart
@@ -0,0 +1,10 @@
+// Copyright 2019 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 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/tasks/web_dev_mode_tests.dart';
+
+Future<void> main() async {
+  await task(createWebDevModeTest());
+}
diff --git a/dev/devicelab/bin/tasks/windows_chrome_dev_mode.dart b/dev/devicelab/bin/tasks/windows_chrome_dev_mode.dart
new file mode 100644
index 0000000..a11b0ab
--- /dev/null
+++ b/dev/devicelab/bin/tasks/windows_chrome_dev_mode.dart
@@ -0,0 +1,10 @@
+// Copyright 2019 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 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/tasks/web_dev_mode_tests.dart';
+
+Future<void> main() async {
+  await task(createWebDevModeTest());
+}
diff --git a/dev/devicelab/lib/tasks/web_dev_mode_tests.dart b/dev/devicelab/lib/tasks/web_dev_mode_tests.dart
new file mode 100644
index 0000000..d5a7ef2
--- /dev/null
+++ b/dev/devicelab/lib/tasks/web_dev_mode_tests.dart
@@ -0,0 +1,142 @@
+// Copyright 2019 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 '../framework/framework.dart';
+import '../framework/utils.dart';
+
+final Directory _editedFlutterGalleryDir = dir(path.join(Directory.systemTemp.path, 'edited_flutter_gallery'));
+final Directory flutterGalleryDir = dir(path.join(flutterDirectory.path, 'examples/flutter_gallery'));
+
+TaskFunction createWebDevModeTest() {
+  return () async {
+    final List<String> options = <String>[
+      '--hot', '-d', 'web', '--verbose', '--resident', '--target=lib/main_web.dart',
+    ];
+    setLocalEngineOptionIfNecessary(options);
+    int hotRestartCount = 0;
+    await inDirectory<void>(flutterDirectory, () async {
+      rmTree(_editedFlutterGalleryDir);
+      mkdirs(_editedFlutterGalleryDir);
+      recursiveCopy(flutterGalleryDir, _editedFlutterGalleryDir);
+      await inDirectory<void>(_editedFlutterGalleryDir, () async {
+        {
+          final Process packagesGet = await startProcess(
+              path.join(flutterDirectory.path, 'bin', 'flutter'),
+              <String>['packages', 'get'],
+              environment: <String, String>{
+                'FLUTTER_WEB': 'true',
+              },
+          );
+          await packagesGet.exitCode;
+          final Process process = await startProcess(
+              path.join(flutterDirectory.path, 'bin', 'flutter'),
+              <String>['run']..addAll(options),
+              environment: <String, String>{
+                'FLUTTER_WEB': 'true',
+              },
+          );
+
+          final Completer<void> stdoutDone = Completer<void>();
+          final Completer<void> stderrDone = Completer<void>();
+          process.stdout
+              .transform<String>(utf8.decoder)
+              .transform<String>(const LineSplitter())
+              .listen((String line) {
+            if (line.contains('To hot restart')) {
+              process.stdin.write('R');
+            }
+            if (line.contains('Restarted')) {
+              if (hotRestartCount == 0) {
+                // Update the file and reload again.
+                final File appDartSource = file(path.join(
+                    _editedFlutterGalleryDir.path, 'lib/gallery/app.dart',
+                ));
+                appDartSource.writeAsStringSync(
+                    appDartSource.readAsStringSync().replaceFirst(
+                        "'Flutter Gallery'", "'Updated Flutter Gallery'",
+                    )
+                );
+                process.stdin.writeln('R');
+                ++hotRestartCount;
+              } else {
+                // Quit after second hot restart.
+                process.stdin.writeln('q');
+              }
+            }
+            print('stdout: $line');
+          }, onDone: () {
+            stdoutDone.complete();
+          });
+          process.stderr
+              .transform<String>(utf8.decoder)
+              .transform<String>(const LineSplitter())
+              .listen((String line) {
+            print('stderr: $line');
+          }, onDone: () {
+            stderrDone.complete();
+          });
+
+          await Future.wait<void>(<Future<void>>[
+            stdoutDone.future,
+            stderrDone.future,
+          ]);
+          await process.exitCode;
+
+        }
+
+        // Start `flutter run` again to make sure it loads from the previous
+        // state. dev compilers loads up from previously compiled JavaScript.
+        {
+          final Process process = await startProcess(
+              path.join(flutterDirectory.path, 'bin', 'flutter'),
+              <String>['run']..addAll(options),
+              environment: <String, String>{
+                'FLUTTER_WEB': 'true',
+              },
+          );
+          final Completer<void> stdoutDone = Completer<void>();
+          final Completer<void> stderrDone = Completer<void>();
+          process.stdout
+              .transform<String>(utf8.decoder)
+              .transform<String>(const LineSplitter())
+              .listen((String line) {
+            if (line.contains('To hot restart')) {
+              process.stdin.write('R');
+            }
+            if (line.contains('Restarted')) {
+              process.stdin.writeln('q');
+            }
+            print('stdout: $line');
+          }, onDone: () {
+            stdoutDone.complete();
+          });
+          process.stderr
+              .transform<String>(utf8.decoder)
+              .transform<String>(const LineSplitter())
+              .listen((String line) {
+            print('stderr: $line');
+          }, onDone: () {
+            stderrDone.complete();
+          });
+
+          await Future.wait<void>(<Future<void>>[
+            stdoutDone.future,
+            stderrDone.future,
+          ]);
+          await process.exitCode;
+        }
+      });
+    });
+    if (hotRestartCount != 1) {
+      return TaskResult.failure(null);
+    }
+    return TaskResult.success(null);
+  };
+}
diff --git a/dev/devicelab/manifest.yaml b/dev/devicelab/manifest.yaml
index 0c976c9..581bbeb 100644
--- a/dev/devicelab/manifest.yaml
+++ b/dev/devicelab/manifest.yaml
@@ -125,6 +125,13 @@
     stage: devicelab_win
     required_agent_capabilities: ["windows/android"]
 
+  windows_chrome_dev_mode:
+    description: >
+      Run flutter web on the devicelab and hot restart.
+    stage: devicelab_win
+    required_agent_capabilities: ["windows/android"]
+    flaky: true
+
   # Android on-device tests
 
   complex_layout_scroll_perf__timeline_summary:
@@ -324,6 +331,13 @@
     stage: devicelab
     required_agent_capabilities: ["linux/android"]
 
+  linux_chrome_dev_mode:
+    description: >
+      Run flutter web on the devicelab and hot restart.
+    stage: devicelab
+    required_agent_capabilities: ["linux/android"]
+    flaky: true
+
   # iOS on-device tests
 
   flavors_test_ios:
@@ -421,6 +435,13 @@
     stage: devicelab_ios
     required_agent_capabilities: ["mac/ios"]
 
+  macos_chrome_dev_mode:
+    description: >
+      Run flutter web on the devicelab and hot restart.
+    stage: devicelab_ios
+    required_agent_capabilities: ["mac/ios"]
+    flaky: true
+
   # Tests running on Windows host
 
   flavors_test_win:
diff --git a/dev/devicelab/pubspec.yaml b/dev/devicelab/pubspec.yaml
index db2cd55..2b1e441 100644
--- a/dev/devicelab/pubspec.yaml
+++ b/dev/devicelab/pubspec.yaml
@@ -8,9 +8,9 @@
   sdk: ">=2.0.0-dev.68.0 <3.0.0"
 
 dependencies:
-  args: 1.5.1
+  args: 1.5.2
   file: 5.0.8
-  image: 2.1.3
+  image: 2.1.4
   meta: 1.1.6
   path: 1.6.2
   platform: 2.2.0
@@ -73,4 +73,4 @@
   watcher: 0.9.7+10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
   yaml: 2.1.15 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
 
-# PUBSPEC CHECKSUM: b018
+# PUBSPEC CHECKSUM: 491a