Fix --pid-file not working for --machine + add to attach command (#23242)

* Fix --pid-file not working for --machine + add to attach

Fixes #23201.

* Add tests for --pid-file in run+attach
diff --git a/packages/flutter_tools/lib/src/base/utils.dart b/packages/flutter_tools/lib/src/base/utils.dart
index a7fe6fa..b9cb836 100644
--- a/packages/flutter_tools/lib/src/base/utils.dart
+++ b/packages/flutter_tools/lib/src/base/utils.dart
@@ -398,6 +398,13 @@
   return result.join('\n');
 }
 
+void writePidFile(String pidFile) {
+  if (pidFile != null) {
+    // Write our pid to the file.
+    fs.file(pidFile).writeAsStringSync(io.pid.toString());
+  }
+}
+
 // Used to represent a run of ANSI control sequences next to a visible
 // character.
 class _AnsiRun {
diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart
index 50091d8..0f1b70a 100644
--- a/packages/flutter_tools/lib/src/commands/attach.dart
+++ b/packages/flutter_tools/lib/src/commands/attach.dart
@@ -7,6 +7,7 @@
 import '../base/common.dart';
 import '../base/file_system.dart';
 import '../base/io.dart';
+import '../base/utils.dart';
 import '../cache.dart';
 import '../commands/daemon.dart';
 import '../device.dart';
@@ -44,6 +45,10 @@
       ..addOption(
         'debug-port',
         help: 'Local port where the observatory is listening.',
+      )..addOption('pid-file',
+        help: 'Specify a file to write the process id to. '
+              'You can send SIGUSR1 to trigger a hot reload '
+              'and SIGUSR2 to trigger a hot restart.',
       )..addOption(
         'project-root',
         hide: !verboseHelp,
@@ -90,6 +95,8 @@
 
     await _validateArguments();
 
+    writePidFile(argResults['pid-file']);
+
     final Device device = await findTargetDevice();
     final int devicePort = observatoryPort;
 
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index b94543e..461c4fc 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -6,7 +6,6 @@
 
 import '../base/common.dart';
 import '../base/file_system.dart';
-import '../base/io.dart';
 import '../base/utils.dart';
 import '../build_info.dart';
 import '../cache.dart';
@@ -276,6 +275,8 @@
     // debug mode.
     final bool hotMode = shouldUseHotMode();
 
+    writePidFile(argResults['pid-file']);
+
     if (argResults['machine']) {
       if (devices.length > 1)
         throwToolExit('--machine does not support -d all.');
@@ -340,12 +341,6 @@
       }
     }
 
-    final String pidFile = argResults['pid-file'];
-    if (pidFile != null) {
-      // Write our pid to the file.
-      fs.file(pidFile).writeAsStringSync(pid.toString());
-    }
-
     final List<FlutterDevice> flutterDevices = devices.map<FlutterDevice>((Device device) {
       return FlutterDevice(
         device,
diff --git a/packages/flutter_tools/test/integration/flutter_attach_test.dart b/packages/flutter_tools/test/integration/flutter_attach_test.dart
index 8b5cff8..2e21ffa 100644
--- a/packages/flutter_tools/test/integration/flutter_attach_test.dart
+++ b/packages/flutter_tools/test/integration/flutter_attach_test.dart
@@ -29,6 +29,15 @@
   });
 
   group('attached process', () {
+    test('writes pid-file', () async {
+      final File pidFile = tempDir.childFile('test.pid');
+      await _flutterRun.run(withDebugger: true);
+      await _flutterAttach.attach(
+        _flutterRun.vmServicePort,
+        pidFile: pidFile,
+      );
+      expect(pidFile.existsSync(), isTrue);
+    });
     test('can hot reload', () async {
       await _flutterRun.run(withDebugger: true);
       await _flutterAttach.attach(_flutterRun.vmServicePort);
diff --git a/packages/flutter_tools/test/integration/flutter_run_test.dart b/packages/flutter_tools/test/integration/flutter_run_test.dart
index 2f1a577..fa594cf 100644
--- a/packages/flutter_tools/test/integration/flutter_run_test.dart
+++ b/packages/flutter_tools/test/integration/flutter_run_test.dart
@@ -9,21 +9,25 @@
 
 import '../src/common.dart';
 import 'test_data/basic_project.dart';
+import 'test_driver.dart';
 import 'test_utils.dart';
 
 void main() {
   group('flutter_run', () {
     Directory tempDir;
     final BasicProject _project = BasicProject();
+    FlutterTestDriver _flutter;
 
     setUp(() async {
       tempDir = createResolvedTempDirectorySync();
       await _project.setUpIn(tempDir);
+      _flutter = FlutterTestDriver(tempDir);
     });
 
     tearDown(() async {
       tryToDelete(tempDir);
     });
+
     test('reports an error if an invalid device is supplied', () async {
       // This test forces flutter to check for all possible devices to catch issues
       // like https://github.com/flutter/flutter/issues/21418 which were skipped
@@ -44,5 +48,11 @@
           fail("'flutter run -d invalid-device-id' did not produce the expected error");
         }
     });
+
+    test('writes pid-file', () async {
+      final File pidFile = tempDir.childFile('test.pid');
+      await _flutter.run(pidFile: pidFile);
+      expect(pidFile.existsSync(), isTrue);
+    });
   }, timeout: const Timeout.factor(6));
 }
diff --git a/packages/flutter_tools/test/integration/test_driver.dart b/packages/flutter_tools/test/integration/test_driver.dart
index a6f535e..c7d89c7 100644
--- a/packages/flutter_tools/test/integration/test_driver.dart
+++ b/packages/flutter_tools/test/integration/test_driver.dart
@@ -56,16 +56,25 @@
     return msg;
   }
 
-  Future<void> run({bool withDebugger = false, bool pauseOnExceptions = false}) async {
+  Future<void> run({
+    bool withDebugger = false,
+    bool pauseOnExceptions = false,
+    File pidFile,
+  }) async {
     await _setupProcess(<String>[
         'run',
         '--machine',
         '-d',
         'flutter-tester',
-    ], withDebugger: withDebugger, pauseOnExceptions: pauseOnExceptions);
+    ], withDebugger: withDebugger, pauseOnExceptions: pauseOnExceptions, pidFile: pidFile);
   }
 
-  Future<void> attach(int port, {bool withDebugger = false, bool pauseOnExceptions = false}) async {
+  Future<void> attach(
+    int port, {
+    bool withDebugger = false,
+    bool pauseOnExceptions = false,
+    File pidFile,
+  }) async {
     await _setupProcess(<String>[
         'attach',
         '--machine',
@@ -73,20 +82,28 @@
         'flutter-tester',
         '--debug-port',
         '$port',
-    ], withDebugger: withDebugger, pauseOnExceptions: pauseOnExceptions);
+    ], withDebugger: withDebugger, pauseOnExceptions: pauseOnExceptions, pidFile: pidFile);
   }
 
-  Future<void> _setupProcess(List<String> args, {bool withDebugger = false, bool pauseOnExceptions = false}) async {
+  Future<void> _setupProcess(
+    List<String> args, {
+    bool withDebugger = false,
+    bool pauseOnExceptions = false,
+    File pidFile,
+  }) async {
     final String flutterBin = fs.path.join(getFlutterRoot(), 'bin', 'flutter');
-    final List<String> flutterArgs = withDebugger
-        ? args.followedBy(<String>['--start-paused']).toList()
-        : args;
-    _debugPrint('Spawning flutter $flutterArgs in ${_projectFolder.path}');
+    if (withDebugger) {
+        args.add('--start-paused');
+    }
+    if (pidFile != null) {
+        args.addAll(<String>['--pid-file', pidFile.path]);
+    }
+    _debugPrint('Spawning flutter $args in ${_projectFolder.path}');
 
     const ProcessManager _processManager = LocalProcessManager();
     _proc = await _processManager.start(
         <String>[flutterBin]
-            .followedBy(flutterArgs)
+            .followedBy(args)
             .toList(),
         workingDirectory: _projectFolder.path,
         environment: <String, String>{'FLUTTER_TEST': 'true'});