Make the startup lock message print to stderr. (#86520)

This changes the "Waiting for another flutter command to release the startup lock..." message output so that it appears on stderr instead of stdout. When it appears on stdout, it can mess up collection of the output. For instance, if you run flutter --version --machine and you're expecting JSON output, then you'll get non-JSON output even though the lock is released and you eventually would get what you're asking for.
diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart
index da4ba9c..d2e0a63 100644
--- a/packages/flutter_tools/lib/src/cache.dart
+++ b/packages/flutter_tools/lib/src/cache.dart
@@ -17,6 +17,7 @@
 import 'base/net.dart';
 import 'base/os.dart' show OperatingSystemUtils;
 import 'base/platform.dart';
+import 'base/terminal.dart';
 import 'base/user_messages.dart';
 import 'build_info.dart';
 import 'convert.dart';
@@ -320,7 +321,13 @@
       } on FileSystemException {
         if (!printed) {
           _logger.printTrace('Waiting to be able to obtain lock of Flutter binary artifacts directory: ${_lock!.path}');
-          _logger.printStatus('Waiting for another flutter command to release the startup lock...');
+          // This needs to go to stderr to avoid cluttering up stdout if a parent
+          // process is collecting stdout. It's not really an "error" though,
+          // so print it in grey.
+          _logger.printError(
+            'Waiting for another flutter command to release the startup lock...',
+            color: TerminalColor.grey,
+          );
           printed = true;
         }
         await Future<void>.delayed(const Duration(milliseconds: 50));
diff --git a/packages/flutter_tools/test/general.shard/cache_test.dart b/packages/flutter_tools/test/general.shard/cache_test.dart
index 2c6850b..ecb7cfe 100644
--- a/packages/flutter_tools/test/general.shard/cache_test.dart
+++ b/packages/flutter_tools/test/general.shard/cache_test.dart
@@ -78,16 +78,22 @@
     });
 
     testWithoutContext('should not throw when lock is acquired', () async {
+      final String oldRoot = Cache.flutterRoot;
       Cache.flutterRoot = '';
-      final FileSystem fileSystem = MemoryFileSystem.test();
-      final Cache cache = Cache.test(fileSystem: fileSystem, processManager: FakeProcessManager.any());
-      fileSystem.file(fileSystem.path.join('bin', 'cache', 'lockfile'))
-        .createSync(recursive: true);
+      try {
+        final FileSystem fileSystem = MemoryFileSystem.test();
+        final Cache cache = Cache.test(
+            fileSystem: fileSystem, processManager: FakeProcessManager.any());
+        fileSystem.file(fileSystem.path.join('bin', 'cache', 'lockfile'))
+            .createSync(recursive: true);
 
-      await cache.lock();
+        await cache.lock();
 
-      expect(cache.checkLockAcquired, returnsNormally);
-      expect(cache.releaseLock, returnsNormally);
+        expect(cache.checkLockAcquired, returnsNormally);
+        expect(cache.releaseLock, returnsNormally);
+      } finally {
+        Cache.flutterRoot = oldRoot;
+      }
     }, skip: true); // TODO(jonahwilliams): implement support for lock so this can be tested with the memory file system.
 
     testWithoutContext('throws tool exit when lockfile open fails', () async {
diff --git a/packages/flutter_tools/test/integration.shard/cache_test.dart b/packages/flutter_tools/test/integration.shard/cache_test.dart
new file mode 100644
index 0000000..c45ece1
--- /dev/null
+++ b/packages/flutter_tools/test/integration.shard/cache_test.dart
@@ -0,0 +1,80 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// @dart = 2.8
+
+import 'dart:io' as io show ProcessSignal;
+
+import 'package:file/file.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/base/terminal.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:process/process.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+import 'test_utils.dart';
+
+final String dart = fileSystem.path
+    .join(getFlutterRoot(), 'bin', platform.isWindows ? 'dart.bat' : 'dart');
+
+void main() {
+  group('Cache.lock', () {
+    // Windows locking is too flaky for this to work reliably.
+    if (!platform.isWindows) {
+      testWithoutContext(
+          'should log a message to stderr when lock is not acquired', () async {
+        final String oldRoot = Cache.flutterRoot;
+        final Directory tempDir = fileSystem.systemTempDirectory.createTempSync('cache_test.');
+        final BufferLogger logger = BufferLogger(
+          terminal: Terminal.test(supportsColor: false, supportsEmoji: false),
+          outputPreferences: OutputPreferences(),
+        );
+        try {
+          Cache.flutterRoot = tempDir.absolute.path;
+          final Cache cache = Cache.test(
+            fileSystem: fileSystem,
+            processManager: FakeProcessManager.any(),
+            logger: logger,
+          );
+          final File cacheFile = fileSystem.file(fileSystem.path
+              .join(Cache.flutterRoot, 'bin', 'cache', 'lockfile'))
+            ..createSync(recursive: true);
+          final File script = fileSystem.file(fileSystem.path
+              .join(Cache.flutterRoot, 'bin', 'cache', 'test_lock.dart'));
+          script.writeAsStringSync(r'''
+import 'dart:async';
+import 'dart:io';
+
+Future<void> main(List<String> args) async {
+  File file = File(args[0]);
+  RandomAccessFile lock = file.openSync(mode: FileMode.write);
+  lock.lockSync();
+  await Future<void>.delayed(const Duration(milliseconds: 1000));
+  exit(0);
+}
+''');
+          final Process process = await const LocalProcessManager().start(
+            <String>[dart, script.absolute.path, cacheFile.absolute.path],
+          );
+          await Future<void>.delayed(const Duration(milliseconds: 500));
+          await cache.lock();
+          process.kill(io.ProcessSignal.sigkill);
+        } finally {
+          try {
+            tempDir.deleteSync(recursive: true);
+          } on FileSystemException {
+            // Ignore filesystem exceptions when trying to delete tempdir.
+          }
+          Cache.flutterRoot = oldRoot;
+        }
+        expect(logger.statusText, isEmpty);
+        expect(logger.errorText,
+            equals('Waiting for another flutter command to release the startup lock...\n'));
+      });
+    }
+  });
+}