[CLI] Account for SDK version changes that happen between when resident frontend compilers are started and when they are used

TEST=test case added to pkg/dartdev/test/commands/run_test.dart

Issue: https://github.com/dart-lang/sdk/issues/54245
Fixes: https://github.com/dart-lang/sdk/issues/55349
Change-Id: Ic3db064d0975e6b999beefed8b6fc32d2c603cff
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/362522
Reviewed-by: Johnni Winther <johnniwinther@google.com>
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Derek Xu <derekx@google.com>
diff --git a/pkg/dartdev/lib/src/generate_kernel.dart b/pkg/dartdev/lib/src/generate_kernel.dart
index c681e9e..00b4fc0 100644
--- a/pkg/dartdev/lib/src/generate_kernel.dart
+++ b/pkg/dartdev/lib/src/generate_kernel.dart
@@ -5,6 +5,7 @@
 import 'dart:io';
 
 import 'package:args/args.dart';
+import 'package:kernel/binary/tag.dart' show isValidSdkHash;
 import 'package:path/path.dart' as p;
 import 'package:pub/pub.dart';
 
@@ -131,7 +132,21 @@
   File serverInfoFile,
 ) async {
   if (serverInfoFile.existsSync()) {
-    return;
+    final residentCompilerInfo = ResidentCompilerInfo.fromFile(serverInfoFile);
+    if (residentCompilerInfo.sdkHash != null &&
+        isValidSdkHash(residentCompilerInfo.sdkHash!)) {
+      // There is already a Resident Frontend Compiler associated with
+      // [serverInfoFile] that is running and compatible with the Dart SDK that
+      // the user is currently using.
+      return;
+    } else {
+      log.stderr(
+        'The Dart SDK has been upgraded or downgraded since the Resident '
+        'Frontend Compiler was started, so the Resident Frontend Compiler will '
+        'now be restarted for compatibility reasons.',
+      );
+      await shutDownOrForgetResidentFrontendCompiler(serverInfoFile);
+    }
   }
   try {
     Directory(p.dirname(serverInfoFile.path)).createSync(recursive: true);
diff --git a/pkg/dartdev/lib/src/resident_frontend_utils.dart b/pkg/dartdev/lib/src/resident_frontend_utils.dart
index 839b085..0fb6830 100644
--- a/pkg/dartdev/lib/src/resident_frontend_utils.dart
+++ b/pkg/dartdev/lib/src/resident_frontend_utils.dart
@@ -52,9 +52,14 @@
 final String packageConfigName = p.join('.dart_tool', 'package_config.json');
 
 final class ResidentCompilerInfo {
+  final String? _sdkHash;
   final InternetAddress _address;
   final int _port;
 
+  /// The SDK hash that kernel files compiled using the Resident Frontend
+  /// Compiler associated with this object will be stamped with.
+  String? get sdkHash => _sdkHash;
+
   /// The address that the Resident Frontend Compiler associated with this
   /// object is listening from.
   InternetAddress get address => _address;
@@ -72,6 +77,9 @@
     final fileContents = file.readAsStringSync();
 
     return ResidentCompilerInfo._(
+      sdkHash: fileContents.contains('sdkHash:')
+          ? _extractValueAssociatedWithKey(fileContents, 'sdkHash')
+          : null,
       address: InternetAddress(
         _extractValueAssociatedWithKey(fileContents, 'address'),
       ),
@@ -80,9 +88,11 @@
   }
 
   ResidentCompilerInfo._({
+    required String? sdkHash,
     required int port,
     required InternetAddress address,
-  })  : _port = port,
+  })  : _sdkHash = sdkHash,
+        _port = port,
         _address = address;
 }
 
diff --git a/pkg/dartdev/pubspec.yaml b/pkg/dartdev/pubspec.yaml
index 723ee14..908f989 100644
--- a/pkg/dartdev/pubspec.yaml
+++ b/pkg/dartdev/pubspec.yaml
@@ -22,6 +22,7 @@
   dtd_impl: any
   front_end: any
   http: any
+  kernel: any
   logging: any
   meta: any
   native_assets_builder: any
diff --git a/pkg/dartdev/test/commands/run_test.dart b/pkg/dartdev/test/commands/run_test.dart
index b51f472..cf9b6db 100644
--- a/pkg/dartdev/test/commands/run_test.dart
+++ b/pkg/dartdev/test/commands/run_test.dart
@@ -8,6 +8,7 @@
 
 import 'package:dartdev/src/resident_frontend_constants.dart';
 import 'package:dartdev/src/resident_frontend_utils.dart';
+import 'package:kernel/binary/tag.dart' show sdkHashNull;
 import 'package:path/path.dart' as path;
 import 'package:pub_semver/pub_semver.dart';
 import 'package:test/test.dart';
@@ -871,15 +872,20 @@
           dartdevKernelCache,
         )).listSync(),
         isNotEmpty);
-  });
 
-  tearDown(() async {
-    try {
-      await sendAndReceiveResponse(
-        residentServerShutdownCommand,
-        File(path.join(serverInfoDirectory.dirPath, 'info')),
-      );
-    } catch (_) {}
+    // [TestProject] uses [addTearDown] to register cleanup code that will
+    // delete the project, and due to ordering guarantees of callbacks
+    // registered using [addTearDown] (refer to [addTearDown]'s doc comment), we
+    // need to use [addTearDown] here to ensure that the resident frontend
+    // compiler we started above will be shut down before the project is
+    // deleted.
+    addTearDown(() async {
+      await serverInfoDirectory.run([
+        'compilation-server',
+        'shutdown',
+        '--$residentCompilerInfoFileOption=$serverInfoFile',
+      ]);
+    });
   });
 
   test(
@@ -970,6 +976,60 @@
     expect(kernelCache, isNotNull);
   });
 
+  test(
+      'a running resident compiler is restarted if the Dart SDK was '
+      'upgraded or downgraded since it was started', () async {
+    p = project(mainSrc: 'void main() {}');
+
+    final residentCompilerInfo = ResidentCompilerInfo.fromFile(
+      File(serverInfoFile),
+    );
+    if (residentCompilerInfo.sdkHash != null &&
+        residentCompilerInfo.sdkHash == sdkHashNull) {
+      // dartdev does not consider the SDK hash changing from [sdkHashNull] to a
+      // different SDK hash to be an SDK upgrade or downgrade. So, if an SDK
+      // of [sdkHashNull] was recorded into [serverInfoFile], the test logic
+      // below will not work as intended. For local developement, we make the
+      // test fail in this situation, alerting the developer that the test is
+      // only meaningful when run from an SDK built with --verify-sdk-hash. The
+      // SDK can only be built with --no-verify-sdk-hash on CI, so we just skip
+      // this test on CI.
+      if (Platform.environment.containsKey('BUILDBOT_BUILDERNAME') ||
+          Platform.environment.containsKey('SWARMING_TASK_ID')) {
+        return;
+      } else {
+        fail(
+          'This test is guaranteed to pass, and thus is not meaningful, when '
+          'run from an SDK built with --no-verify-sdk-hash',
+        );
+      }
+    }
+
+    // Replace the SDK hash in [serverInfoFile] to make dartdev think the Dart
+    // SDK version has changed since the resident compiler was started.
+    File(serverInfoFile).writeAsStringSync(
+      'address:${residentCompilerInfo.address.address} '
+      'sdkHash:${'1' * residentCompilerInfo.sdkHash!.length} '
+      'port:${residentCompilerInfo.port} ',
+    );
+    final result = await p.run([
+      'run',
+      '--resident',
+      '--$residentCompilerInfoFileOption=$serverInfoFile',
+      p.relativeFilePath,
+    ]);
+
+    expect(result.exitCode, 0);
+    expect(result.stdout, contains(residentFrontendCompilerPrefix));
+    expect(
+      result.stderr,
+      'The Dart SDK has been upgraded or downgraded since the Resident '
+      'Frontend Compiler was started, so the Resident Frontend Compiler will '
+      'now be restarted for compatibility reasons.\n',
+    );
+    expect(File(serverInfoFile).existsSync(), true);
+  });
+
   test('when a connection to a running resident compiler cannot be established',
       () async {
     // When this occurs, the user should be informed that the resident frontend
@@ -979,7 +1039,9 @@
     // that a connection will not be established.
     final testServerInfoFile = File(path.join(p.dirPath, 'info'));
     testServerInfoFile.createSync();
-    testServerInfoFile.writeAsStringSync('address:127.0.0.1 port:-12 ');
+    testServerInfoFile.writeAsStringSync(
+      'address:127.0.0.1 sdkHash:$sdkHashNull port:-12 ',
+    );
     final result = await p.run([
       'run',
       '--resident',
diff --git a/pkg/frontend_server/lib/src/resident_frontend_server.dart b/pkg/frontend_server/lib/src/resident_frontend_server.dart
index 1a893df..99bb559 100644
--- a/pkg/frontend_server/lib/src/resident_frontend_server.dart
+++ b/pkg/frontend_server/lib/src/resident_frontend_server.dart
@@ -17,6 +17,7 @@
 
 import 'package:args/args.dart';
 import 'package:front_end/src/api_unstable/vm.dart';
+import 'package:kernel/binary/tag.dart' show expectedSdkHash;
 
 import '../frontend_server.dart';
 
@@ -461,8 +462,21 @@
       throw new StateError('A server is already running.');
     }
     server = await ServerSocket.bind(address, port);
+    // There are particular aspects of the info file format that must be
+    // preserved to ensure backwards compatibility with the original versions
+    // of the utilities for parsing this file.
+    //
+    // The aspects of the info file format that must be preserved are:
+    // 1. The file must begin with 'address:$address '. Note that $address IS
+    //    NOT preceded by a space and IS followed by a space.
+    // 2. The file must end with 'port:$port'. Note that $port IS NOT preceded
+    //    by a space. $port may be followed by zero or more whitespace
+    //    characters.
     serverInfoFile.writeAsStringSync(
-        'address:${server.address.address} port:${server.port}');
+      'address:${server.address.address} '
+      'sdkHash:${expectedSdkHash} '
+      'port:${server.port} ',
+    );
   } on StateError catch (e) {
     print('Error: $e\n');
     return null;