[ResidentFrontendServer] Add 'replaceCachedDill' endpoint

TEST=test case added to
`pkg/frontend_server/test/src/resident_frontend_server_test.dart`

Change-Id: I45a4b28c8c89714dcb93fa7f64874e158a0d69df
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/394763
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Derek Xu <derekx@google.com>
diff --git a/pkg/dartdev/lib/src/commands/run.dart b/pkg/dartdev/lib/src/commands/run.dart
index 5ae7891..17e97c7 100644
--- a/pkg/dartdev/lib/src/commands/run.dart
+++ b/pkg/dartdev/lib/src/commands/run.dart
@@ -8,6 +8,8 @@
 import 'package:args/args.dart';
 import 'package:front_end/src/api_prototype/compiler_options.dart'
     show Verbosity;
+import 'package:frontend_server/resident_frontend_server_utils.dart'
+    show invokeReplaceCachedDill;
 import 'package:path/path.dart';
 import 'package:pub/pub.dart';
 
@@ -428,8 +430,26 @@
       }
 
       final executableFile = File(executable.executable);
-      if (!await isFileKernelFile(executableFile) &&
-          !await isFileAppJitSnapshot(executableFile) &&
+      if (await isFileKernelFile(executableFile)) {
+        // If the file is a kernel file, we do not need to compile it, but we do
+        // need to replace the file in the resident frontend compiler kernel
+        // cache associated with this executable, because the cached kernel file
+        // may be used to populate context for expression evaluation later.
+        await ensureCompilationServerIsRunning(residentCompilerInfoFile);
+        final succeeded = await invokeReplaceCachedDill(
+          replacementDillPath: executableFile.absolute.path,
+          serverInfoFile: residentCompilerInfoFile,
+        );
+        if (!succeeded) {
+          log.stderr(
+            'Error: Encountered a problem accessing the Resident Frontend '
+            "Compiler's kernel file cache. Please try re-running the same "
+            'command again. If the error persists, please file an issue at '
+            'https://github.com/dart-lang/sdk/issues/new.',
+          );
+          return errorExitCode;
+        }
+      } else if (!await isFileAppJitSnapshot(executableFile) &&
           !await isFileAotSnapshot(executableFile)) {
         final compiledKernelFile = await _compileToKernelUsingResidentCompiler(
           executable: executable,
diff --git a/pkg/frontend_server/lib/resident_frontend_server_utils.dart b/pkg/frontend_server/lib/resident_frontend_server_utils.dart
index a1382ae..20efd4d 100644
--- a/pkg/frontend_server/lib/resident_frontend_server_utils.dart
+++ b/pkg/frontend_server/lib/resident_frontend_server_utils.dart
@@ -2,7 +2,7 @@
 // for details. 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:convert' show jsonDecode;
+import 'dart:convert' show jsonDecode, jsonEncode;
 import 'dart:io' show Directory, File, InternetAddress, Socket;
 
 import 'package:path/path.dart' as path;
@@ -112,3 +112,22 @@
   client?.destroy();
   return jsonResponse;
 }
+
+/// Sends a 'replaceCachedDill' request with [replacementDillPath] as the lone
+/// argument to the resident frontend compiler associated with [serverInfoFile],
+/// and returns a boolean indicating whether or not replacement succeeded.
+///
+/// Throws a [FileSystemException] if [serverInfoFile] cannot be accessed.
+Future<bool> invokeReplaceCachedDill({
+  required String replacementDillPath,
+  required File serverInfoFile,
+}) async {
+  final Map<String, dynamic> response = await sendAndReceiveResponse(
+    jsonEncode({
+      'command': 'replaceCachedDill',
+      'replacementDillPath': replacementDillPath,
+    }),
+    serverInfoFile,
+  );
+  return response['success'];
+}
diff --git a/pkg/frontend_server/lib/src/resident_frontend_server.dart b/pkg/frontend_server/lib/src/resident_frontend_server.dart
index c6e2c51..18b44b8b 100644
--- a/pkg/frontend_server/lib/src/resident_frontend_server.dart
+++ b/pkg/frontend_server/lib/src/resident_frontend_server.dart
@@ -18,6 +18,7 @@
 import 'package:args/args.dart';
 import 'package:front_end/src/api_unstable/vm.dart';
 import 'package:kernel/binary/tag.dart' show expectedSdkHash;
+import 'package:kernel/kernel.dart' show Component, loadComponentFromBytes;
 import 'package:path/path.dart' as path;
 
 import '../frontend_server.dart';
@@ -89,6 +90,10 @@
     updateState(_compileOptions);
   }
 
+  void resetStateToWaitingForFirstCompile() {
+    _state = _ResidentState.waitingForFirstCompile;
+  }
+
   /// The [ResidentCompiler] will use the [newOptions] for future compilation
   /// requests.
   void updateState(ArgResults newOptions) {
@@ -99,7 +104,7 @@
     // Refresh the compiler's output for the next compile
     _compilerOutput.clear();
     _formattedOutput.clear();
-    _state = _ResidentState.waitingForFirstCompile;
+    resetStateToWaitingForFirstCompile();
   }
 
   /// The current compiler options are outdated when any option has changed
@@ -170,7 +175,7 @@
         ..resetIncrementalCompiler();
       _state = _ResidentState.waitingForRecompile;
     } else {
-      _state = _ResidentState.waitingForFirstCompile;
+      resetStateToWaitingForFirstCompile();
     }
 
     return _createResponseMap(
@@ -256,6 +261,8 @@
 /// residentListenAndCompile method.
 class ResidentFrontendServer {
   static const String _commandString = 'command';
+  static const String _replaceCachedDillString = 'replaceCachedDill';
+  static const String _replacementDillPathString = 'replacementDillPath';
   static const String _compileString = 'compile';
   static const String _executableString = 'executable';
   static const String _packageString = 'packages';
@@ -301,6 +308,43 @@
     return residentCompiler;
   }
 
+  static Future<String> _handleReplaceCachedDillRequest(
+    Map<String, dynamic> request,
+  ) async {
+    if (request[_replacementDillPathString] == null) {
+      return _encodeErrorMessage(
+        "'$_replaceCachedDillString' requests must include a "
+        "'$_replacementDillPathString' property.",
+      );
+    }
+
+    final File replacementDillFile =
+        new File(request[_replacementDillPathString]);
+
+    final String canonicalizedLibraryPath;
+    try {
+      final Component component =
+          loadComponentFromBytes(replacementDillFile.readAsBytesSync());
+      canonicalizedLibraryPath = path.canonicalize(
+        component.mainMethod!.enclosingLibrary.fileUri.toFilePath(),
+      );
+
+      final String cachedDillPath =
+          computeCachedDillPath(canonicalizedLibraryPath);
+      replacementDillFile.copySync(cachedDillPath);
+    } catch (e) {
+      return _encodeErrorMessage('Failed to replace cached dill');
+    }
+
+    if (compilers[canonicalizedLibraryPath] != null) {
+      compilers[canonicalizedLibraryPath]!.resetStateToWaitingForFirstCompile();
+    }
+
+    return jsonEncode({
+      "success": true,
+    });
+  }
+
   static Future<String> _handleCompileRequest(
     Map<String, dynamic> request,
   ) async {
@@ -365,6 +409,8 @@
     }
 
     switch (request[_commandString]) {
+      case _replaceCachedDillString:
+        return _handleReplaceCachedDillRequest(request);
       case _compileString:
         return _handleCompileRequest(request);
       case _shutdownString:
diff --git a/pkg/frontend_server/test/src/resident_frontend_server_test.dart b/pkg/frontend_server/test/src/resident_frontend_server_test.dart
index bf01c6a..1b7cf14 100644
--- a/pkg/frontend_server/test/src/resident_frontend_server_test.dart
+++ b/pkg/frontend_server/test/src/resident_frontend_server_test.dart
@@ -103,7 +103,72 @@
     });
   });
 
-  group('Resident Frontend Server: compile tests: ', () {
+  group("Resident Frontend Server: 'replaceCachedDill' command tests: ", () {
+    late Directory d;
+    late File executable, outputDill;
+
+    setUp(() async {
+      d = Directory.systemTemp.createTempSync();
+      executable = new File(path.join(d.path, 'src.dart'))
+        ..createSync()
+        ..writeAsStringSync('void main() {print("hello " "there");}');
+      outputDill = new File(path.join(d.path, 'src.dart.dill'));
+    });
+
+    tearDown(() async {
+      d.deleteSync(recursive: true);
+      ResidentFrontendServer.compilers.clear();
+    });
+
+    test('basic', () async {
+      final File cachedDillFile =
+          new File(computeCachedDillPath(executable.path));
+      expect(cachedDillFile.existsSync(), false);
+
+      final Map<String, dynamic> compileResult =
+          jsonDecode(await ResidentFrontendServer.handleRequest(
+        ResidentFrontendServer.createCompileJSON(
+          executable: executable.path,
+          outputDill: outputDill.path,
+        ),
+      ));
+      expect(compileResult['success'], true);
+
+      expect(cachedDillFile.existsSync(), true);
+      // Delete the kernel file associated with [executable.path] from the
+      // resident frontend compiler kernel cache.
+      cachedDillFile.deleteSync();
+
+      final Map<String, dynamic> replaceCachedDillResult = jsonDecode(
+        await ResidentFrontendServer.handleRequest(
+          jsonEncode({
+            'command': 'replaceCachedDill',
+            'replacementDillPath': outputDill.path,
+          }),
+        ),
+      );
+      expect(replaceCachedDillResult['success'], true);
+      // Calling 'replaceCachedDill' with [outputDill] as the replacement dill
+      // should make [outputDill] the kernel file associated with
+      // [executable.path] in the resident frontend compiler kernel cache.
+      expect(cachedDillFile.existsSync(), true);
+      cachedDillFile.deleteSync();
+    });
+
+    test("invalid 'replacementDillPath' property in request", () async {
+      final Map<String, dynamic> replaceCachedDillResult = jsonDecode(
+        await ResidentFrontendServer.handleRequest(
+          jsonEncode({
+            'command': 'replaceCachedDill',
+            'replacementDillPath': path.join(d.path, 'nonexistent'),
+          }),
+        ),
+      );
+      expect(replaceCachedDillResult['success'], false);
+    });
+  });
+
+  group("Resident Frontend Server: 'compile' command tests: ", () {
     late Directory d;
     late File executable, package, outputDill;