[dartdev] This adds the ability for the Dart CLI to use the Resident Frontend Compiler for faster start times.

Dartdev will use the user's home directory to store the server information and will keep a directory for cached kernel files in each dart package's .dart_tool directory and in the .dart directory for stand alone dart programs.

This functionality is accessed by providing the --resident flag to the Dart CLI, and the server can be manually shutdown with the new shutdown command.

Change-Id: I5231a00b7535266ab0704ca3ae35c039738bd38b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/254341
Reviewed-by: Jake Macdonald <jakemac@google.com>
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Michael Richards <msrichards@google.com>
Reviewed-by: Siva Annamalai <asiva@google.com>
diff --git a/pkg/dartdev/lib/dartdev.dart b/pkg/dartdev/lib/dartdev.dart
index f6f37dd..15a276c 100644
--- a/pkg/dartdev/lib/dartdev.dart
+++ b/pkg/dartdev/lib/dartdev.dart
@@ -18,6 +18,7 @@
 import 'src/analytics.dart';
 import 'src/commands/analyze.dart';
 import 'src/commands/compile.dart';
+import 'src/commands/compile_server_shutdown.dart';
 import 'src/commands/create.dart';
 import 'src/commands/debug_adapter.dart';
 import 'src/commands/devtools.dart';
@@ -142,6 +143,7 @@
     );
     addCommand(RunCommand(verbose: verbose));
     addCommand(TestCommand());
+    addCommand(CompileServerShutdownCommand(verbose: verbose));
   }
 
   @visibleForTesting
diff --git a/pkg/dartdev/lib/src/commands/compile_server_shutdown.dart b/pkg/dartdev/lib/src/commands/compile_server_shutdown.dart
new file mode 100644
index 0000000..efaf515
--- /dev/null
+++ b/pkg/dartdev/lib/src/commands/compile_server_shutdown.dart
@@ -0,0 +1,59 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// 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:async';
+import 'dart:io' show File, FileSystemException;
+
+import 'package:args/args.dart';
+
+import '../core.dart';
+import '../resident_frontend_constants.dart';
+import '../resident_frontend_utils.dart';
+import '../utils.dart';
+
+/// Implement `dart compiler-server-shutdown`.
+class CompileServerShutdownCommand extends DartdevCommand {
+  static const cmdName = 'compiler-server-shutdown';
+
+  CompileServerShutdownCommand({bool verbose = false})
+      : super(cmdName, 'Shut down the Resident Frontend Compiler.', false) {
+    argParser.addOption(
+      'resident-server-info-file',
+      hide: !verbose,
+      help: 'Specify the file that the Dart CLI uses to communicate with '
+          'the Resident Frontend Compiler. Passing this flag results in '
+          'having one unique resident frontend compiler per file. '
+          'This is needed when writing unit '
+          'tests that utilize resident mode in order to maintain isolation.',
+    );
+  }
+
+  // This argument parser is here solely to ensure that VM specific flags are
+  // provided before any command and to provide a more consistent help message
+  // with the rest of the tool.
+  @override
+  ArgParser createArgParser() {
+    return ArgParser();
+  }
+
+  @override
+  FutureOr<int> run() async {
+    final args = argResults!;
+    final serverInfoFile = args.wasParsed(serverInfoOption)
+        ? File(maybeUriToFilename(args[serverInfoOption]))
+        : File(defaultResidentServerInfoFile);
+    try {
+      final serverResponse = await sendAndReceiveResponse(
+        residentServerShutdownCommand,
+        serverInfoFile,
+      );
+      cleanupResidentServerInfo(serverInfoFile);
+      if (serverResponse.containsKey(responseErrorString)) {
+        log.stderr(serverResponse[responseErrorString]);
+        return DartdevCommand.errorExitCode;
+      }
+    } on FileSystemException catch (_) {} // no server is running
+    return 0;
+  }
+}
diff --git a/pkg/dartdev/lib/src/commands/run.dart b/pkg/dartdev/lib/src/commands/run.dart
index f029757..e22de71 100644
--- a/pkg/dartdev/lib/src/commands/run.dart
+++ b/pkg/dartdev/lib/src/commands/run.dart
@@ -15,6 +15,9 @@
 
 import '../core.dart';
 import '../experiments.dart';
+import '../generate_kernel.dart';
+import '../resident_frontend_constants.dart';
+import '../resident_frontend_utils.dart';
 import '../sdk.dart';
 import '../utils.dart';
 import '../vm_interop_handler.dart';
@@ -44,6 +47,23 @@
           'Run a Dart program.',
           verbose,
         ) {
+    argParser
+      ..addFlag(
+        'resident',
+        abbr: 'r',
+        negatable: false,
+        help: 'Enable faster startup times with the '
+            'Resident Frontend Compiler.',
+      )
+      ..addOption(
+        'resident-server-info-file',
+        hide: !verbose,
+        help: 'Specify the file that the Dart CLI uses to communicate with '
+            'the Resident Frontend Compiler. Passing this flag results in '
+            'having one unique resident frontend compiler per file. '
+            'This is needed when writing unit '
+            'tests that utilize resident mode in order to maintain isolation.',
+      );
     // NOTE: When updating this list of flags, be sure to add any VM flags to
     // the list of flags in Options::ProcessVMDebuggingOptions in
     // runtime/bin/main_options.cc. Failure to do so will result in those VM
@@ -254,27 +274,59 @@
       }
     }
 
+    final hasServerInfoOption = args.wasParsed(
+      serverInfoOption,
+    );
+    final useResidentServer =
+        args.wasParsed(residentOption) || hasServerInfoOption;
+    DartExecutableWithPackageConfig executable;
     try {
-      final executable = await getExecutableForCommand(mainCommand);
-      VmInteropHandler.run(
-        executable.executable,
-        runArgs,
-        packageConfigOverride: executable.packageConfig,
+      executable = await getExecutableForCommand(
+        mainCommand,
+        allowSnapshot: !useResidentServer,
       );
-      return 0;
     } on CommandResolutionFailedException catch (e) {
       log.stderr(e.message);
       return errorExitCode;
     }
-  }
-}
 
-/// Try parsing [maybeUri] as a file uri or [maybeUri] itself if that fails.
-String maybeUriToFilename(String maybeUri) {
-  try {
-    return Uri.parse(maybeUri).toFilePath();
-  } catch (_) {
-    return maybeUri;
+    if (useResidentServer) {
+      final serverInfoFile = hasServerInfoOption
+          ? File(maybeUriToFilename(args[serverInfoOption]))
+          : File(defaultResidentServerInfoFile);
+      try {
+        // TODO(#49694) handle the case when executable is a kernel file
+        executable = await generateKernel(
+          executable,
+          serverInfoFile,
+          args,
+          createCompileJitJson,
+        );
+      } on FrontendCompilerException catch (e) {
+        log.stderr('${ansi.yellow}Failed to build '
+            '${executable.executable}:${ansi.none}');
+        log.stderr(e.message);
+        if (e.issue == CompilationIssue.serverError) {
+          try {
+            await sendAndReceiveResponse(
+              residentServerShutdownCommand,
+              serverInfoFile,
+            );
+          } catch (_) {
+          } finally {
+            cleanupResidentServerInfo(serverInfoFile);
+          }
+        }
+        return errorExitCode;
+      }
+    }
+
+    VmInteropHandler.run(
+      executable.executable,
+      runArgs,
+      packageConfigOverride: executable.packageConfig,
+    );
+    return 0;
   }
 }
 
diff --git a/pkg/dartdev/lib/src/commands/test.dart b/pkg/dartdev/lib/src/commands/test.dart
index 31d7916..d12c689 100644
--- a/pkg/dartdev/lib/src/commands/test.dart
+++ b/pkg/dartdev/lib/src/commands/test.dart
@@ -50,7 +50,7 @@
         print('');
         printUsage();
       }
-      return 65;
+      return DartdevCommand.errorExitCode;
     }
   }
 }
diff --git a/pkg/dartdev/lib/src/core.dart b/pkg/dartdev/lib/src/core.dart
index dab9d59..971c287 100644
--- a/pkg/dartdev/lib/src/core.dart
+++ b/pkg/dartdev/lib/src/core.dart
@@ -27,6 +27,8 @@
 void Function(ArgParser argParser, String cmdName)? flagContributor;
 
 abstract class DartdevCommand extends Command<int> {
+  static const errorExitCode = 65;
+
   final String _name;
   final String _description;
   final bool _verbose;
diff --git a/pkg/dartdev/lib/src/generate_kernel.dart b/pkg/dartdev/lib/src/generate_kernel.dart
new file mode 100644
index 0000000..fa01f19
--- /dev/null
+++ b/pkg/dartdev/lib/src/generate_kernel.dart
@@ -0,0 +1,224 @@
+// Copyright 2022 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:io';
+
+import 'package:args/args.dart';
+import 'package:package_config/package_config.dart';
+import 'package:path/path.dart' as p;
+import 'package:pub/pub.dart';
+
+import 'resident_frontend_constants.dart';
+import 'resident_frontend_utils.dart';
+import 'sdk.dart';
+
+typedef CompileRequestGeneratorCallback = String Function({
+  required String executable,
+  required String outputDill,
+  required ArgResults args,
+  String? packages,
+});
+
+/// Uses the resident frontend compiler to compute a kernel file for
+/// [executable]. Throws a [FrontendCompilerException] if the compilation
+/// fails or if the source code contians compilation errors.
+///
+/// [executable] is expected to contain a path to the dart source file and
+/// a package_config file.
+///
+/// [serverInfoFile] is the location that should be checked to find an existing
+/// Resident Frontend Compiler. If one does not exist, a server is created and
+/// its address and port information is written to this file location.
+///
+/// [args] is the [ArgResults] object that is created by the DartDev commands.
+/// This is where the optional path override for the serverInfoFile is passed
+/// in.
+///
+/// [compileRequestGenerator] is applied to produce a request for the Resident
+/// Frontend Server.
+Future<DartExecutableWithPackageConfig> generateKernel(
+  DartExecutableWithPackageConfig executable,
+  File serverInfoFile,
+  ArgResults args,
+  CompileRequestGeneratorCallback compileRequestGenerator, {
+  bool aot = false,
+}) async {
+  // Locates the package_config.json and cached kernel file, makes sure the
+  // resident frontend server is up and running, and computes a kernel.
+  final packageRoot = _packageRootFor(executable);
+  if (packageRoot == null) {
+    throw FrontendCompilerException._(
+        'resident mode is only supported for Dart packages.',
+        CompilationIssue.standaloneProgramError);
+  }
+  await _ensureCompileServerIsRunning(args, serverInfoFile);
+  // TODO: allow custom package paths with a --packages flag
+  final packageConfig = await _resolvePackageConfig(executable, packageRoot);
+  final cachedKernel = _cachedKernelPath(executable.executable, packageRoot);
+  Map<String, dynamic> result;
+  try {
+    result = await sendAndReceiveResponse(
+      compileRequestGenerator(
+        executable: p.canonicalize(executable.executable),
+        outputDill: cachedKernel,
+        packages: packageConfig,
+        args: args,
+      ),
+      serverInfoFile,
+    );
+  } on FileSystemException catch (e) {
+    throw FrontendCompilerException._(e.message, CompilationIssue.serverError);
+  }
+  if (!result[responseSuccessString]) {
+    if (result.containsKey(responseErrorString)) {
+      throw FrontendCompilerException._(
+        result[responseErrorString],
+        CompilationIssue.serverError,
+      );
+    } else {
+      throw FrontendCompilerException._(
+        (result[responseOutputString] as List<dynamic>).join('\n'),
+        CompilationIssue.compilationError,
+      );
+    }
+  }
+  return DartExecutableWithPackageConfig(
+    executable: cachedKernel,
+    packageConfig: packageConfig,
+  );
+}
+
+/// Returns the absolute path to [executable]'s cached kernel file.
+/// Throws a [FrontendCompilerException] if the cached kernel cannot be
+/// created.
+String _cachedKernelPath(String executable, String packageRoot) {
+  final executableDirPath = p.canonicalize(p.dirname(executable));
+  var cachedKernelDirectory = p.join(
+    packageRoot,
+    '.dart_tool',
+    dartdevKernelCache,
+  );
+
+  final subdirectoryList =
+      executableDirPath.replaceFirst(packageRoot, '').split(p.separator);
+  for (var directory in subdirectoryList) {
+    cachedKernelDirectory = p.join(cachedKernelDirectory, directory);
+  }
+
+  try {
+    Directory(cachedKernelDirectory).createSync(recursive: true);
+  } catch (e) {
+    throw FrontendCompilerException._(
+      e.toString(),
+      CompilationIssue.serverError,
+    );
+  }
+  return p.canonicalize(
+    p.join(
+      cachedKernelDirectory,
+      '${p.basename(executable)}-${sdk.version}.dill',
+    ),
+  );
+}
+
+/// Ensures that the Resident Frontend Compiler is running, starting it if
+/// necessary. Throws a [FrontendCompilerException] if starting the server
+/// fails.
+Future<void> _ensureCompileServerIsRunning(
+  ArgResults args,
+  File serverInfoFile,
+) async {
+  if (serverInfoFile.existsSync()) {
+    return;
+  }
+  try {
+    Directory(p.dirname(serverInfoFile.path)).createSync(recursive: true);
+    // TODO replace this with the AOT executable when that is built.
+    final frontendServerProcess = await Process.start(
+      sdk.dart,
+      [
+        sdk.frontendServerSnapshot,
+        '--resident-info-file-name=${serverInfoFile.path}'
+      ],
+      workingDirectory: home,
+      mode: ProcessStartMode.detachedWithStdio,
+    );
+
+    final serverOutput =
+        String.fromCharCodes(await frontendServerProcess.stdout.first);
+    if (serverOutput.startsWith('Error')) {
+      throw StateError(serverOutput);
+    }
+    print(serverOutput); // Prints the server's address and port information
+  } catch (e) {
+    throw FrontendCompilerException._(
+      e.toString(),
+      CompilationIssue.serverCreationError,
+    );
+  }
+}
+
+/// Returns the path to the root of the [executable]'s package, or null
+/// if it is a standalone dart file.
+String? _packageRootFor(DartExecutableWithPackageConfig executable) {
+  Directory currentDirectory =
+      Directory(p.dirname(p.canonicalize(executable.executable)));
+
+  while (currentDirectory.parent.path != currentDirectory.path) {
+    if (File(p.join(currentDirectory.path, 'pubspec.yaml')).existsSync() ||
+        File(p.join(currentDirectory.path, packageConfigName)).existsSync()) {
+      return currentDirectory.path;
+    }
+    currentDirectory = currentDirectory.parent;
+  }
+  return null;
+}
+
+/// Resolves the absolute path to [packageRoot]'s package_config.json file,
+/// returning null if the package does not contain one, or if the source
+/// being compiled is a standalone dart script not inside a package.
+Future<String?> _resolvePackageConfig(
+    DartExecutableWithPackageConfig executable, String packageRoot) async {
+  final packageConfig = await findPackageConfigUri(
+    Uri.file(p.canonicalize(executable.executable)),
+    recurse: true,
+    onError: (_) {},
+  );
+  if (packageConfig != null) {
+    final dotPackageFile = File(p.join(packageRoot, '.packages'));
+    final packageConfigFile = File(p.join(packageRoot, packageConfigName));
+    return packageConfigFile.existsSync()
+        ? packageConfigFile.path
+        : dotPackageFile.path;
+  }
+  return null;
+}
+
+/// Indicates the type of issue encountered with the
+/// Resident Frontend Compiler
+enum CompilationIssue {
+  /// Communication with the Resident Frontend Compiler failed.
+  serverError,
+
+  /// The Resident Frontend Compiler failed to launch
+  serverCreationError,
+
+  /// There were compilation errors in the Dart source code.
+  compilationError,
+
+  /// Resident mode is only supported for sources within Dart packages
+  standaloneProgramError,
+}
+
+/// Indicates an error with the Resident Frontend Compiler.
+class FrontendCompilerException implements Exception {
+  final String message;
+  final CompilationIssue issue;
+  FrontendCompilerException._(this.message, this.issue);
+
+  @override
+  String toString() {
+    return 'FrontendCompilerException: $message';
+  }
+}
diff --git a/pkg/dartdev/lib/src/resident_frontend_constants.dart b/pkg/dartdev/lib/src/resident_frontend_constants.dart
new file mode 100644
index 0000000..8306f15
--- /dev/null
+++ b/pkg/dartdev/lib/src/resident_frontend_constants.dart
@@ -0,0 +1,20 @@
+/// The name of the subdirectory in .dart_tool where dartdev stores kernel files
+const dartdevKernelCache = 'kernel';
+const serverInfoOption = 'resident-server-info-file';
+const residentOption = 'resident';
+const responseSuccessString = 'success';
+const responseErrorString = 'errorMessage';
+const responseOutputString = 'compilerOutputLines';
+const commandString = 'command';
+const compileString = 'compile';
+const shutdownString = 'shutdown';
+const sourceString = 'executable';
+const outputString = 'output-dill';
+const packageString = 'packages';
+const defineOption = 'define';
+const enableAssertsOption = 'enable-asserts';
+const enableExperimentOption = 'enable-experiment';
+const soundNullSafetyOption = 'sound-null-safety';
+const aotOption = 'aot';
+const tfaOption = 'tfa';
+const verbosityOption = 'verbosity';
diff --git a/pkg/dartdev/lib/src/resident_frontend_utils.dart b/pkg/dartdev/lib/src/resident_frontend_utils.dart
new file mode 100644
index 0000000..3b10c93
--- /dev/null
+++ b/pkg/dartdev/lib/src/resident_frontend_utils.dart
@@ -0,0 +1,143 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// 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';
+import 'dart:io'
+    show File, FileSystemException, InternetAddress, Platform, Socket;
+import 'package:args/args.dart';
+import 'package:path/path.dart' as p;
+
+import 'resident_frontend_constants.dart';
+
+/// The Resident Frontend Compiler's shutdown command.
+final residentServerShutdownCommand = jsonEncode(
+  <String, Object>{
+    commandString: shutdownString,
+  },
+);
+
+/// The path to the user's home directory
+///
+/// TODO: The current implementation gives 1 server to a user
+///   and stores the info file in the .dart directory in the user's home.
+///   This adds some fragility to the --resident command as it expects this
+///   environment variable to exist.
+///   If/when the resident frontend compiler is used without requiring the
+///   --resident flag, this reliance on the environment variable should be
+///   addressed.
+final home = Platform.isWindows
+    ? Platform.environment['USERPROFILE']!
+    : Platform.environment['HOME']!;
+
+/// The path to the directory that stores the Resident Frontend Compiler's
+/// information file, which stores the server's address and port number.
+///
+/// File has the form: address:__ port:__
+final defaultResidentServerInfoFile =
+    p.join(home, '.dart', 'dartdev_compilation_server_info');
+
+final packageConfigName = p.join('.dart_tool', 'package_config.json');
+
+/// Get the port number the Resident Frontend Compiler is listening on.
+int getPortNumber(String serverInfo) =>
+    int.parse(serverInfo.substring(serverInfo.lastIndexOf(':') + 1));
+
+/// Get the address that the Resident Frontend Compiler is listening from.
+InternetAddress getAddress(String serverInfo) => InternetAddress(
+    serverInfo.substring(serverInfo.indexOf(':') + 1, serverInfo.indexOf(' ')));
+
+/// Removes the [serverInfoFile].
+void cleanupResidentServerInfo(File serverInfoFile) {
+  if (serverInfoFile.existsSync()) {
+    try {
+      serverInfoFile.deleteSync();
+    } catch (_) {}
+  }
+}
+
+// TODO: when frontend_server is migrated to null safe Dart, everything
+// below this comment can be removed and imported from resident_frontend_server
+
+/// Sends a compilation [request] to the Resident Frontend Compiler, returning
+/// it's json response.
+///
+/// Throws a [FileSystemException] if there is no server running.
+Future<Map<String, dynamic>> sendAndReceiveResponse(
+  String request,
+  File serverInfoFile,
+) async {
+  Socket? client;
+  Map<String, dynamic> jsonResponse;
+  final contents = serverInfoFile.readAsStringSync();
+  try {
+    client =
+        await Socket.connect(getAddress(contents), getPortNumber(contents));
+    client.write(request);
+    final data = String.fromCharCodes(await client.first);
+    jsonResponse = jsonDecode(data);
+  } catch (e) {
+    jsonResponse = <String, dynamic>{
+      responseSuccessString: false,
+      responseErrorString: e.toString(),
+    };
+  }
+  client?.destroy();
+  return jsonResponse;
+}
+
+/// Used to create compile requests for the run CLI command.
+/// Returns a JSON string that the resident compiler will be able to
+/// interpret.
+String createCompileJitJson({
+  required String executable,
+  required String outputDill,
+  required ArgResults args,
+  String? packages,
+  bool verbose = false,
+}) {
+  return jsonEncode(
+    <String, Object?>{
+      commandString: compileString,
+      sourceString: executable,
+      outputString: outputDill,
+      if (args.wasParsed(defineOption)) defineOption: args[defineOption],
+      if (args.wasParsed(enableAssertsOption)) enableAssertsOption: true,
+      if (args.wasParsed(enableExperimentOption))
+        enableExperimentOption: args[enableExperimentOption],
+      if (packages != null) packageString: packages,
+      if (args.wasParsed(verbosityOption))
+        verbosityOption: args[verbosityOption],
+    },
+  );
+}
+
+/// Used to create compile requesets for AOT compilations
+String createCompileAotJson({
+  required String executable,
+  required String outputDill,
+  required ArgResults args,
+  String? packages,
+  bool productMode = false,
+  bool verbose = false,
+}) {
+  return jsonEncode(
+    <String, Object?>{
+      commandString: compileString,
+      sourceString: executable,
+      outputString: outputDill,
+      aotOption: true,
+      tfaOption: true,
+      if (productMode) '-Ddart.vm.product': true,
+      if (args.wasParsed(defineOption)) defineOption: args[defineOption],
+      if (args.wasParsed(enableExperimentOption))
+        enableExperimentOption: args[enableExperimentOption],
+      if (args.wasParsed(packageString)) packageString: args[packageString],
+      if (!args.wasParsed(packageString) && packages != null)
+        packageString: packages,
+      if (args.wasParsed(soundNullSafetyOption)) soundNullSafetyOption: true,
+      if (args.wasParsed(verbosityOption))
+        verbosityOption: args[verbosityOption],
+    },
+  );
+}
diff --git a/pkg/dartdev/lib/src/sdk.dart b/pkg/dartdev/lib/src/sdk.dart
index a994d9e..e79df9a 100644
--- a/pkg/dartdev/lib/src/sdk.dart
+++ b/pkg/dartdev/lib/src/sdk.dart
@@ -50,6 +50,12 @@
         'dds.dart.snapshot',
       );
 
+  String get frontendServerSnapshot => path.absolute(
+        sdkPath,
+        'bin',
+        'snapshots',
+        'frontend_server.dart.snapshot',
+      );
   String get devToolsBinaries => path.absolute(
         sdkPath,
         'bin',
diff --git a/pkg/dartdev/lib/src/utils.dart b/pkg/dartdev/lib/src/utils.dart
index a80f7bd..c2e9cc5 100644
--- a/pkg/dartdev/lib/src/utils.dart
+++ b/pkg/dartdev/lib/src/utils.dart
@@ -11,6 +11,15 @@
 int? get dartdevUsageLineLength =>
     stdout.hasTerminal ? stdout.terminalColumns : null;
 
+/// Try parsing [maybeUri] as a file uri or [maybeUri] itself if that fails.
+String maybeUriToFilename(String maybeUri) {
+  try {
+    return Uri.parse(maybeUri).toFilePath();
+  } catch (_) {
+    return maybeUri;
+  }
+}
+
 /// Given a data structure which is a Map of String to dynamic values, return
 /// the same structure (`Map<String, dynamic>`) with the correct runtime types.
 Map<String, dynamic> castStringKeyedMap(dynamic untyped) {
diff --git a/pkg/dartdev/pubspec.yaml b/pkg/dartdev/pubspec.yaml
index 58afb2a..2be0c70 100644
--- a/pkg/dartdev/pubspec.yaml
+++ b/pkg/dartdev/pubspec.yaml
@@ -21,6 +21,7 @@
   front_end: any
   meta: any
   nnbd_migration: any
+  package_config: any
   path: any
   pub: any
   telemetry: any
diff --git a/pkg/dartdev/test/commands/compile_server_shutdown_test.dart b/pkg/dartdev/test/commands/compile_server_shutdown_test.dart
new file mode 100644
index 0000000..801a845
--- /dev/null
+++ b/pkg/dartdev/test/commands/compile_server_shutdown_test.dart
@@ -0,0 +1,58 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// 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:io';
+
+import 'package:dartdev/src/resident_frontend_constants.dart';
+import 'package:path/path.dart' as path;
+import 'package:test/test.dart';
+
+import '../utils.dart';
+
+void main() {
+  group('shutdown', () {
+    late TestProject p;
+
+    tearDown(() async => await p.dispose());
+
+    test('shutdown issued with no server running', () async {
+      p = project();
+      final serverInfoFile = path.join(p.dirPath, 'info');
+      final result = await p.run([
+        'compiler-server-shutdown',
+        '--$serverInfoOption=$serverInfoFile',
+      ]);
+
+      expect(result.stdout, isEmpty);
+      expect(result.stderr, isEmpty);
+      expect(result.exitCode, 0);
+      expect(File(serverInfoFile).existsSync(), false);
+    });
+
+    test('shutdown', () async {
+      p = project(mainSrc: 'void main() {}');
+      final serverInfoFile = path.join(p.dirPath, 'info');
+      final runResult = await p.run([
+        'run',
+        '--$serverInfoOption=$serverInfoFile',
+        p.relativeFilePath,
+      ]);
+
+      expect(runResult.stdout, isNotEmpty);
+      expect(runResult.stderr, isEmpty);
+      expect(runResult.exitCode, 0);
+      expect(File(serverInfoFile).existsSync(), true);
+
+      final result = await p.run([
+        'compiler-server-shutdown',
+        '--$serverInfoOption=$serverInfoFile',
+      ]);
+
+      expect(result.stdout, isEmpty);
+      expect(result.stderr, isEmpty);
+      expect(result.exitCode, 0);
+      expect(File(serverInfoFile).existsSync(), false);
+    });
+  }, timeout: longTimeout);
+}
diff --git a/pkg/dartdev/test/commands/run_test.dart b/pkg/dartdev/test/commands/run_test.dart
index fc8bee9..778e28e 100644
--- a/pkg/dartdev/test/commands/run_test.dart
+++ b/pkg/dartdev/test/commands/run_test.dart
@@ -6,6 +6,8 @@
 import 'dart:convert';
 import 'dart:io';
 
+import 'package:dartdev/src/resident_frontend_constants.dart';
+import 'package:dartdev/src/resident_frontend_utils.dart';
 import 'package:path/path.dart' as path;
 import 'package:test/test.dart';
 
@@ -16,9 +18,12 @@
     'The Dart DevTools debugger and profiler is available at: http://127.0.0.1:';
 const dartVMServiceMessagePrefix =
     'The Dart VM service is listening on http://127.0.0.1:';
+const residentFrontendServerPrefix =
+    'The Resident Frontend Compiler is listening at 127.0.0.1:';
 
-void main() {
+void main() async {
   group('run', run, timeout: longTimeout);
+  group('run --resident', residentRun, timeout: longTimeout);
 }
 
 void run() {
@@ -538,3 +543,317 @@
     );
   });
 }
+
+void residentRun() {
+  late TestProject serverInfoDirectory, p;
+  late String serverInfoFile;
+
+  setUpAll(() async {
+    serverInfoDirectory = project(mainSrc: 'void main() {}');
+    serverInfoFile = path.join(serverInfoDirectory.dirPath, 'info');
+    final result = await serverInfoDirectory.run([
+      'run',
+      '--$serverInfoOption=$serverInfoFile',
+      serverInfoDirectory.relativeFilePath,
+    ]);
+    expect(result.exitCode, 0);
+    expect(File(serverInfoFile).existsSync(), true);
+    expect(
+        Directory(path.join(
+          serverInfoDirectory.dirPath,
+          '.dart_tool',
+          dartdevKernelCache,
+        )).listSync(),
+        isNotEmpty);
+  });
+
+  tearDownAll(() async {
+    try {
+      await sendAndReceiveResponse(
+        residentServerShutdownCommand,
+        File(path.join(serverInfoDirectory.dirPath, 'info')),
+      );
+    } catch (_) {}
+
+    serverInfoDirectory.dispose();
+  });
+
+  tearDown(() async => await p.dispose());
+
+  test("'Hello World'", () async {
+    p = project(mainSrc: "void main() { print('Hello World'); }");
+    final result = await p.run([
+      'run',
+      '--$serverInfoOption=$serverInfoFile',
+      p.relativeFilePath,
+    ]);
+    Directory? kernelCache = p.findDirectory('.dart_tool/kernel');
+
+    expect(result.exitCode, 0);
+    expect(
+      result.stdout,
+      allOf(
+        contains('Hello World'),
+        isNot(contains(residentFrontendServerPrefix)),
+      ),
+    );
+    expect(result.stderr, isEmpty);
+    expect(kernelCache, isNot(null));
+  });
+
+  test('same server used from different directories', () async {
+    p = project(mainSrc: "void main() { print('1'); }");
+    TestProject p2 = project(mainSrc: "void main() { print('2'); }");
+    addTearDown(() async => p2.dispose());
+
+    final runResult1 = await p.run([
+      'run',
+      '--$serverInfoOption=$serverInfoFile',
+      p.relativeFilePath,
+    ]);
+    final runResult2 = await p2.run([
+      'run',
+      '--$serverInfoOption=$serverInfoFile',
+      p2.relativeFilePath,
+    ]);
+
+    expect(runResult1.exitCode, allOf(0, equals(runResult2.exitCode)));
+    expect(
+      runResult1.stdout,
+      allOf(
+        contains('1'),
+        isNot(contains(residentFrontendServerPrefix)),
+      ),
+    );
+    expect(
+      runResult2.stdout,
+      allOf(
+        contains('2'),
+        isNot(contains(residentFrontendServerPrefix)),
+      ),
+    );
+  });
+
+  test('kernel cache respects directory structure', () async {
+    p = project(name: 'foo');
+    p.file('lib/main.dart', 'void main() {}');
+    p.file('bin/main.dart', 'void main() {}');
+
+    final runResult1 = await p.run([
+      'run',
+      '--$serverInfoOption=$serverInfoFile',
+      path.join(p.dirPath, 'lib/main.dart'),
+    ]);
+    expect(runResult1.exitCode, 0);
+    expect(runResult1.stdout, isEmpty);
+    expect(runResult1.stderr, isEmpty);
+
+    final runResult2 = await p.run([
+      'run',
+      '--$serverInfoOption=$serverInfoFile',
+      path.join(p.dirPath, 'bin/main.dart'),
+    ]);
+    expect(runResult2.exitCode, 0);
+    expect(runResult2.stdout, isEmpty);
+    expect(runResult2.stderr, isEmpty);
+
+    final cache = p.findDirectory('.dart_tool/kernel');
+    expect(cache, isNot(null));
+    expect(Directory(path.join(cache!.path, 'lib')).existsSync(), true);
+    expect(Directory(path.join(cache.path, 'bin')).existsSync(), true);
+  });
+
+  test('standalone dart program', () async {
+    p = project(mainSrc: 'void main() {}');
+    p.deleteFile('pubspec.yaml');
+    final runResult = await p.run([
+      'run',
+      '--$serverInfoOption=$serverInfoFile',
+      p.relativeFilePath,
+    ]);
+
+    expect(runResult.stderr,
+        contains('resident mode is only supported for Dart packages.'));
+    expect(runResult.exitCode, isNot(0));
+    expect(File(serverInfoFile).existsSync(), true);
+  });
+
+  test('directory that the server is started in is deleted', () async {
+    // The first command will start the server process in the p2
+    // project directory.
+    // This directory is deleted. The second command will attempt to run again
+    // The server process should not fail on this second attempt. If it does,
+    // the 3rd command will result in a new server starting.
+    Directory tempServerInfoDir = Directory.systemTemp.createTempSync('a');
+    String tempServerInfoFile = path.join(tempServerInfoDir.path, 'info');
+    addTearDown(() async {
+      try {
+        await sendAndReceiveResponse(
+          residentServerShutdownCommand,
+          File(tempServerInfoFile),
+        );
+      } catch (_) {}
+      await deleteDirectory(tempServerInfoDir);
+    });
+    p = project(mainSrc: 'void main() {}');
+    TestProject p2 = project(mainSrc: 'void main() {}');
+    final runResult1 = await p2.run([
+      'run',
+      '--$serverInfoOption=$tempServerInfoFile',
+      p2.relativeFilePath,
+    ]);
+    await deleteDirectory(p2.dir);
+    expect(runResult1.exitCode, 0);
+    expect(runResult1.stdout, contains(residentFrontendServerPrefix));
+
+    await p.run([
+      'run',
+      '--$serverInfoOption=$tempServerInfoFile',
+      p.relativeFilePath,
+    ]);
+    final runResult2 = await p.run([
+      'run',
+      '--$serverInfoOption=$tempServerInfoFile',
+      p.relativeFilePath,
+    ]);
+
+    expect(runResult2.exitCode, 0);
+    expect(runResult2.stderr, isEmpty);
+    expect(runResult2.stdout, isNot(contains(residentFrontendServerPrefix)));
+  });
+
+  test('VM flags are passed properly', () async {
+    p = project(mainSrc: "void main() { print('Hello World'); }");
+
+    // --observe sets the following flags by default:
+    //   --enable-vm-service
+    //   --pause-isolate-on-exit
+    //   --pause-isolate-on-unhandled-exception
+    //   --warn-on-pause-with-no-debugger
+    //
+    // This test ensures that allowed arguments for dart run which are valid VM
+    // arguments are properly handled by the VM.
+    ProcessResult result = await p.run([
+      'run',
+      '--$serverInfoOption=$serverInfoFile',
+      '--observe',
+      '--pause-isolates-on-start',
+      // This should negate the above flag.
+      '--no-pause-isolates-on-start',
+      '--no-pause-isolates-on-exit',
+      '--no-pause-isolates-on-unhandled-exceptions',
+      '-Dfoo=bar',
+      '--define=bar=foo',
+      p.relativeFilePath,
+    ]);
+
+    expect(
+      result.stdout,
+      isNot(
+        matches(
+            r'The Resident Frontend Compiler is listening at 127.0.0.1:[0-9]+'),
+      ),
+    );
+    expect(
+      result.stdout,
+      matches(
+          r'The Dart VM service is listening on http:\/\/127.0.0.1:8181\/[a-zA-Z0-9_-]+=\/\n.*'),
+    );
+    expect(result.stderr, isEmpty);
+    expect(result.exitCode, 0);
+
+    // Again, with --disable-service-auth-codes.
+    result = await p.run([
+      'run',
+      '--$serverInfoOption=$serverInfoFile',
+      '--observe',
+      '--pause-isolates-on-start',
+      // This should negate the above flag.
+      '--no-pause-isolates-on-start',
+      '--no-pause-isolates-on-exit',
+      '--no-pause-isolates-on-unhandled-exceptions',
+      '--disable-service-auth-codes',
+      '-Dfoo=bar',
+      '--define=bar=foo',
+      p.relativeFilePath,
+    ]);
+
+    expect(
+      result.stdout,
+      isNot(
+        matches(
+            r'The Resident Frontend Compiler is listening at 127.0.0.1:[0-9]+'),
+      ),
+    );
+    expect(
+      result.stdout,
+      contains('The Dart VM service is listening on http://127.0.0.1:8181/\n'),
+    );
+    expect(result.stderr, isEmpty);
+    expect(result.exitCode, 0);
+
+    // Again, with IPv6.
+    result = await p.run([
+      'run',
+      '--$serverInfoOption=$serverInfoFile',
+      '--observe=8181/::1',
+      '--pause-isolates-on-start',
+      // This should negate the above flag.
+      '--no-pause-isolates-on-start',
+      '--no-pause-isolates-on-exit',
+      '--no-pause-isolates-on-unhandled-exceptions',
+      '-Dfoo=bar',
+      '--define=bar=foo',
+      p.relativeFilePath,
+    ]);
+
+    expect(
+      result.stdout,
+      isNot(
+        matches(
+            r'The Resident Frontend Compiler is listening at 127.0.0.1:[0-9]+'),
+      ),
+    );
+    expect(
+      result.stdout,
+      matches(
+          r'The Dart VM service is listening on http:\/\/\[::1\]:8181\/[a-zA-Z0-9_-]+=\/\n.*'),
+    );
+    expect(result.stderr, isEmpty);
+    expect(result.exitCode, 0);
+  });
+
+  test('custom package_config path', () async {
+    p = project(name: 'foo');
+    final bar = TestProject(name: 'bar');
+    final baz = TestProject(name: 'baz', mainSrc: '''
+  import 'package:bar/bar.dart'
+  void main() {}
+''');
+    addTearDown(() async => bar.dispose());
+    addTearDown(() async => baz.dispose());
+
+    p.file('custom_packages.json', '''
+{
+  "configVersion": 2,
+  "packages": [
+    {
+      "name": "bar",
+      "rootUri": "${bar.dirPath}",
+      "packageUri": "${path.join(bar.dirPath, 'lib')}"
+    }
+  ]
+}
+''');
+    final runResult = await baz.run([
+      'run',
+      '--$serverInfoOption=$serverInfoFile',
+      '--packages=${path.join(p.dirPath, 'custom_packages.json')}',
+      baz.relativeFilePath,
+    ]);
+
+    expect(runResult.exitCode, 0);
+    expect(runResult.stderr, isEmpty);
+    expect(runResult.stdout, isEmpty);
+  }, skip: 'until a --packages flag is added to the run command');
+}
diff --git a/pkg/dartdev/test/utils.dart b/pkg/dartdev/test/utils.dart
index f159c9b..69ce380 100644
--- a/pkg/dartdev/test/utils.dart
+++ b/pkg/dartdev/test/utils.dart
@@ -108,22 +108,7 @@
     _process?.kill();
     await _process?.exitCode;
     _process = null;
-    int deleteAttempts = 5;
-    while (deleteAttempts >= 0) {
-      deleteAttempts--;
-      try {
-        if (!dir.existsSync()) {
-          return;
-        }
-        dir.deleteSync(recursive: true);
-      } catch (e) {
-        if (deleteAttempts <= 0) {
-          rethrow;
-        }
-        await Future.delayed(Duration(milliseconds: 500));
-        print('Got $e while deleting $dir. Trying again...');
-      }
-    }
+    await deleteDirectory(dir);
   }
 
   Future<ProcessResult> run(
@@ -205,3 +190,22 @@
     return file.existsSync() ? file : null;
   }
 }
+
+Future<void> deleteDirectory(Directory dir) async {
+  int deleteAttempts = 5;
+  while (deleteAttempts >= 0) {
+    deleteAttempts--;
+    try {
+      if (!dir.existsSync()) {
+        return;
+      }
+      dir.deleteSync(recursive: true);
+    } catch (e) {
+      if (deleteAttempts <= 0) {
+        rethrow;
+      }
+      await Future.delayed(Duration(milliseconds: 500));
+      log.stdout('Got $e while deleting $dir. Trying again...');
+    }
+  }
+}