// 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.

import 'dart:async';

import 'package:meta/meta.dart';
import 'package:unified_analytics/unified_analytics.dart' as analytics;
import 'package:vm_service/vm_service.dart';

import '../android/android_device.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../build_info.dart';
import '../device.dart';
import '../features.dart';
import '../globals.dart' as globals;
import '../ios/devices.dart';
import '../project.dart';
import '../resident_runner.dart';
import '../run_cold.dart';
import '../run_hot.dart';
import '../runner/flutter_command.dart';
import '../runner/flutter_command_runner.dart';
import '../tracing.dart';
import '../web/compile.dart';
import '../web/web_constants.dart';
import '../web/web_runner.dart';
import 'daemon.dart';

/// Shared logic between `flutter run` and `flutter drive` commands.
abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
  RunCommandBase({required bool verboseHelp}) {
    addBuildModeFlags(verboseHelp: verboseHelp, defaultToRelease: false);
    usesDartDefineOption();
    usesFlavorOption();
    usesWebResourcesCdnFlag();
    addNativeNullAssertions(hide: !verboseHelp);
    usesApplicationBinaryOption();
    argParser
      ..addFlag(
        'trace-startup',
        negatable: false,
        help:
            'Trace application startup, then exit, saving the trace to a file. '
            'By default, this will be saved in the "build" directory. If the '
            'FLUTTER_TEST_OUTPUTS_DIR environment variable is set, the file '
            'will be written there instead.',
      )
      ..addFlag(
        'cache-startup-profile',
        help:
            'Caches the CPU profile collected before the first frame for startup '
            'analysis.',
      )
      ..addFlag(
        'verbose-system-logs',
        negatable: false,
        help: 'Include verbose logging from the Flutter engine.',
      )
      ..addFlag(
        'purge-persistent-cache',
        negatable: false,
        help:
            'Removes all existing persistent caches. This allows reproducing '
            'shader compilation jank that normally only happens the first time '
            'an app is run, or for reliable testing of compilation jank fixes '
            '(e.g. shader warm-up).',
      )
      ..addOption('route', help: 'Which route to load when running the app.')
      ..addOption(
        'vmservice-out-file',
        help:
            'A file to write the attached vmservice URL to after an '
            'application is started.',
        valueHelp: 'project/example/out.txt',
        hide: !verboseHelp,
      )
      ..addFlag(
        'disable-service-auth-codes',
        negatable: false,
        hide: !verboseHelp,
        help:
            '(deprecated) Allow connections to the VM service without using authentication codes. '
            '(Not recommended! This can open your device to remote code execution attacks!)',
      )
      ..addFlag(
        'start-paused',
        defaultsTo: startPausedDefault,
        help: 'Start in a paused mode and wait for a debugger to connect.',
      )
      ..addOption(
        'dart-flags',
        hide: !verboseHelp,
        help:
            'Pass a list of comma separated flags to the Dart instance at '
            'application startup. Flags passed through this option must be '
            'present on the allowlist defined within the Flutter engine. If '
            'a disallowed flag is encountered, the process will be '
            'terminated immediately.\n\n'
            'This flag is not available on the stable channel and is only '
            'applied in debug and profile modes. This option should only '
            'be used for experiments and should not be used by typical users.',
      )
      ..addFlag(
        'endless-trace-buffer',
        negatable: false,
        help:
            'Enable tracing to an infinite buffer, instead of a ring buffer. '
            'This is useful when recording large traces. To use an endless buffer to '
            'record startup traces, combine this with "--trace-startup".',
      )
      ..addFlag(
        'trace-systrace',
        negatable: false,
        help:
            'Enable tracing to the system tracer. This is only useful on '
            'platforms where such a tracer is available (Android, iOS, '
            'macOS and Fuchsia).',
      )
      ..addOption(
        'trace-to-file',
        help:
            'Write the timeline trace to a file at the specified path. The '
            "file will be in Perfetto's proto format; it will be possible to "
            "load the file into Perfetto's trace viewer.",
        valueHelp: 'path/to/trace.binpb',
      )
      ..addFlag(
        'profile-microtasks',
        negatable: false,
        help:
            'Enable collection of information about each microtask. '
            'Information about completed microtasks will be written to the '
            '"Microtask" timeline stream. Information about queued microtasks '
            'will be accessible from Dart / Flutter DevTools.',
        hide: !verboseHelp,
      )
      ..addFlag(
        'trace-skia',
        negatable: false,
        help:
            'Enable tracing of Skia code. This is useful when debugging '
            'the raster thread (formerly known as the GPU thread). '
            'By default, Flutter will not log Skia code, as it introduces significant '
            'overhead that may affect recorded performance metrics in a misleading way.',
      )
      ..addOption(
        'trace-allowlist',
        hide: !verboseHelp,
        help:
            'Filters out all trace events except those that are specified in '
            'this comma separated list of allowed prefixes.',
        valueHelp: 'foo,bar',
      )
      ..addOption(
        'trace-skia-allowlist',
        hide: !verboseHelp,
        help:
            'Filters out all Skia trace events except those that are specified in '
            'this comma separated list of allowed prefixes.',
        valueHelp: 'skia.gpu,skia.shaders',
      )
      ..addFlag(
        'enable-dart-profiling',
        defaultsTo: true,
        help:
            'Whether the Dart VM sampling CPU profiler is enabled. This flag '
            'is only meaningful in debug and profile builds.',
      )
      ..addFlag(
        'enable-software-rendering',
        negatable: false,
        help:
            '(deprecated) Enable rendering using the Skia software backend. '
            'This is useful when testing Flutter on emulators. By default, '
            'Flutter will attempt to either use OpenGL or Vulkan and fall back '
            'to software when neither is available. This option is not supported '
            'when using the Impeller rendering engine.',
        hide: !verboseHelp,
      )
      ..addFlag(
        'skia-deterministic-rendering',
        negatable: false,
        help:
            '(deprecated) When combined with "--enable-software-rendering", this should provide completely '
            'deterministic (i.e. reproducible) Skia rendering. This is useful for testing purposes '
            '(e.g. when comparing screenshots). This option is not supported '
            'when using the Impeller rendering engine.',
        hide: !verboseHelp,
      )
      ..addMultiOption(
        'dart-entrypoint-args',
        abbr: 'a',
        help:
            'Pass a list of arguments to the Dart entrypoint at application '
            'startup. By default this is main(List<String> args). Specify '
            'this option multiple times each with one argument to pass '
            'multiple arguments to the Dart entrypoint. Currently this is '
            'only supported on desktop platforms.',
      )
      ..addFlag(
        'uninstall-first',
        hide: !verboseHelp,
        help:
            'Uninstall previous versions of the app on the device '
            'before reinstalling. Currently only supported on iOS.',
      )
      ..addFlag(
        FlutterOptions.kWebWasmFlag,
        help: 'Compile to WebAssembly rather than JavaScript.\n$kWasmMoreInfo',
        negatable: false,
      );
    usesWebOptions(verboseHelp: verboseHelp);
    usesTargetOption();
    usesPortOptions(verboseHelp: verboseHelp);
    usesIpv6Flag(verboseHelp: verboseHelp);
    usesPubOption();
    usesTrackWidgetCreation(verboseHelp: verboseHelp);
    usesDeviceUserOption();
    usesDeviceTimeoutOption();
    usesDeviceConnectionOption();
    addDdsOptions(verboseHelp: verboseHelp);
    addDevToolsOptions(verboseHelp: verboseHelp);
    addAndroidSpecificBuildOptions(hide: !verboseHelp);
    usesFatalWarningsOption(verboseHelp: verboseHelp);
    addEnableImpellerFlag(verboseHelp: verboseHelp);
    addEnableFlutterGpuFlag(verboseHelp: verboseHelp);
    addEnableVulkanValidationFlag(verboseHelp: verboseHelp);
    addEnableEmbedderApiFlag(verboseHelp: verboseHelp);
  }

  bool get traceStartup => boolArg('trace-startup');
  bool get enableDartProfiling => boolArg('enable-dart-profiling');
  bool get purgePersistentCache => boolArg('purge-persistent-cache');
  bool get disableServiceAuthCodes => boolArg('disable-service-auth-codes');
  bool get cacheStartupProfile => boolArg('cache-startup-profile');
  bool get runningWithPrebuiltApplication =>
      argResults![FlutterOptions.kUseApplicationBinary] != null;
  bool get trackWidgetCreation => boolArg('track-widget-creation');
  ImpellerStatus get enableImpeller =>
      ImpellerStatus.fromBool(argResults!['enable-impeller'] as bool?);
  bool get enableFlutterGpu => (argResults!['enable-flutter-gpu'] as bool?) ?? false;
  bool get enableVulkanValidation => boolArg('enable-vulkan-validation');
  bool get uninstallFirst => boolArg('uninstall-first');
  bool get enableEmbedderApi => boolArg('enable-embedder-api');

  @override
  bool get refreshWirelessDevices => true;

  /// Whether to start the application paused by default.
  bool get startPausedDefault;

  String? get route => stringArg('route');

  String? get traceAllowlist => stringArg('trace-allowlist');

  bool get useWasm => boolArg(FlutterOptions.kWebWasmFlag);

  // Keep in sync with the [TestCommand.webRenderer] getter.
  WebRendererMode get webRenderer {
    final List<String> dartDefines = extractDartDefines(
      defineConfigJsonMap: extractDartDefineConfigJsonMap(),
    );
    return WebRendererMode.fromDartDefines(dartDefines, useWasm: useWasm);
  }

  /// Create a debugging options instance for the current `run` or `drive` invocation.
  @visibleForTesting
  @protected
  Future<DebuggingOptions> createDebuggingOptions(bool webMode) async {
    final BuildInfo buildInfo = await getBuildInfo();
    final int? webBrowserDebugPort =
        featureFlags.isWebEnabled && argResults!.wasParsed('web-browser-debug-port')
        ? int.parse(stringArg('web-browser-debug-port')!)
        : null;
    final List<String> webBrowserFlags = featureFlags.isWebEnabled
        ? stringsArg(FlutterOptions.kWebBrowserFlag)
        : const <String>[];

    final Map<String, String> webHeaders = featureFlags.isWebEnabled
        ? extractWebHeaders()
        : const <String, String>{};

    if (buildInfo.mode.isRelease) {
      return DebuggingOptions.disabled(
        buildInfo,
        dartEntrypointArgs: stringsArg('dart-entrypoint-args'),
        hostname: featureFlags.isWebEnabled ? stringArg('web-hostname') : '',
        port: featureFlags.isWebEnabled ? stringArg('web-port') : '',
        tlsCertPath: featureFlags.isWebEnabled ? stringArg('web-tls-cert-path') : null,
        tlsCertKeyPath: featureFlags.isWebEnabled ? stringArg('web-tls-cert-key-path') : null,
        webUseSseForDebugProxy:
            featureFlags.isWebEnabled && stringArg('web-server-debug-protocol') == 'sse',
        webUseSseForDebugBackend:
            featureFlags.isWebEnabled && stringArg('web-server-debug-backend-protocol') == 'sse',
        webUseSseForInjectedClient:
            featureFlags.isWebEnabled &&
            stringArg('web-server-debug-injected-client-protocol') == 'sse',
        webEnableExposeUrl: featureFlags.isWebEnabled && boolArg('web-allow-expose-url'),
        webRunHeadless: featureFlags.isWebEnabled && boolArg('web-run-headless'),
        webBrowserDebugPort: webBrowserDebugPort,
        webBrowserFlags: webBrowserFlags,
        webHeaders: webHeaders,
        webRenderer: webRenderer,
        webUseWasm: useWasm,
        enableImpeller: enableImpeller,
        enableFlutterGpu: enableFlutterGpu,
        enableVulkanValidation: enableVulkanValidation,
        uninstallFirst: uninstallFirst,
        enableDartProfiling: enableDartProfiling,
        enableEmbedderApi: enableEmbedderApi,
        usingCISystem: usingCISystem,
        debugLogsDirectoryPath: debugLogsDirectoryPath,
      );
    } else {
      return DebuggingOptions.enabled(
        buildInfo,
        startPaused: boolArg('start-paused'),
        disableServiceAuthCodes: boolArg('disable-service-auth-codes'),
        cacheStartupProfile: cacheStartupProfile,
        enableDds: enableDds,
        dartEntrypointArgs: stringsArg('dart-entrypoint-args'),
        dartFlags: stringArg('dart-flags') ?? '',
        useTestFonts: argParser.options.containsKey('use-test-fonts') && boolArg('use-test-fonts'),
        enableSoftwareRendering:
            argParser.options.containsKey('enable-software-rendering') &&
            boolArg('enable-software-rendering'),
        skiaDeterministicRendering:
            argParser.options.containsKey('skia-deterministic-rendering') &&
            boolArg('skia-deterministic-rendering'),
        traceSkia: boolArg('trace-skia'),
        traceAllowlist: traceAllowlist,
        traceSkiaAllowlist: stringArg('trace-skia-allowlist'),
        traceSystrace: boolArg('trace-systrace'),
        traceToFile: stringArg('trace-to-file'),
        endlessTraceBuffer: boolArg('endless-trace-buffer'),
        profileMicrotasks: boolArg('profile-microtasks'),
        purgePersistentCache: purgePersistentCache,
        deviceVmServicePort: deviceVmservicePort,
        hostVmServicePort: hostVmservicePort,
        disablePortPublication: await disablePortPublication,
        ddsPort: ddsPort,
        devToolsServerAddress: devToolsServerAddress,
        verboseSystemLogs: boolArg('verbose-system-logs'),
        hostname: featureFlags.isWebEnabled ? stringArg('web-hostname') : '',
        port: featureFlags.isWebEnabled ? stringArg('web-port') : '',
        tlsCertPath: featureFlags.isWebEnabled ? stringArg('web-tls-cert-path') : null,
        tlsCertKeyPath: featureFlags.isWebEnabled ? stringArg('web-tls-cert-key-path') : null,
        webUseSseForDebugProxy:
            featureFlags.isWebEnabled && stringArg('web-server-debug-protocol') == 'sse',
        webUseSseForDebugBackend:
            featureFlags.isWebEnabled && stringArg('web-server-debug-backend-protocol') == 'sse',
        webUseSseForInjectedClient:
            featureFlags.isWebEnabled &&
            stringArg('web-server-debug-injected-client-protocol') == 'sse',
        webEnableExposeUrl: featureFlags.isWebEnabled && boolArg('web-allow-expose-url'),
        webRunHeadless: featureFlags.isWebEnabled && boolArg('web-run-headless'),
        webBrowserDebugPort: webBrowserDebugPort,
        webBrowserFlags: webBrowserFlags,
        webEnableExpressionEvaluation:
            featureFlags.isWebEnabled && boolArg('web-enable-expression-evaluation'),
        webLaunchUrl: featureFlags.isWebEnabled ? stringArg('web-launch-url') : null,
        webHeaders: webHeaders,
        webRenderer: webRenderer,
        webUseWasm: useWasm,
        vmserviceOutFile: stringArg('vmservice-out-file'),
        fastStart:
            argParser.options.containsKey('fast-start') &&
            boolArg('fast-start') &&
            !runningWithPrebuiltApplication,
        nativeNullAssertions: boolArg('native-null-assertions'),
        enableImpeller: enableImpeller,
        enableFlutterGpu: enableFlutterGpu,
        enableVulkanValidation: enableVulkanValidation,
        uninstallFirst: uninstallFirst,
        enableDartProfiling: enableDartProfiling,
        enableEmbedderApi: enableEmbedderApi,
        usingCISystem: usingCISystem,
        debugLogsDirectoryPath: debugLogsDirectoryPath,
        enableDevTools: boolArg(FlutterCommand.kEnableDevTools),
        ipv6: boolArg(FlutterCommand.ipv6Flag),
        printDtd: boolArg(FlutterGlobalOptions.kPrintDtd, global: true),
      );
    }
  }
}

class RunCommand extends RunCommandBase {
  RunCommand({bool verboseHelp = false}) : super(verboseHelp: verboseHelp) {
    requiresPubspecYaml();
    usesFilesystemOptions(hide: !verboseHelp);
    usesExtraDartFlagOptions(verboseHelp: verboseHelp);
    usesFrontendServerStarterPathOption(verboseHelp: verboseHelp);
    addEnableExperimentation(hide: !verboseHelp);
    usesInitializeFromDillOption(hide: !verboseHelp);
    usesNativeAssetsOption(hide: !verboseHelp);

    // By default, the app should to publish the VM service port over mDNS.
    // This will allow subsequent "flutter attach" commands to connect to the VM
    // without needing to know the port.
    addPublishPort(verboseHelp: verboseHelp);
    addIgnoreDeprecationOption();
    addMachineOutputFlag(verboseHelp: verboseHelp);
    argParser
      ..addFlag(
        'await-first-frame-when-tracing',
        defaultsTo: true,
        help:
            'Whether to wait for the first frame when tracing startup ("--trace-startup"), '
            'or just dump the trace as soon as the application is running. The first frame '
            'is detected by looking for a Timeline event with the name '
            '"${Tracing.firstUsefulFrameEventName}". '
            "By default, the widgets library's binding takes care of sending this event.",
      )
      ..addFlag(
        'use-test-fonts',
        help:
            'Enable (and default to) the "Ahem" font. This is a special font '
            'used in tests to remove any dependencies on the font metrics. It '
            'is enabled when you use "flutter test". Set this flag when running '
            'a test using "flutter run" for debugging purposes. This flag is '
            'only available when running in debug mode.',
      )
      ..addFlag('build', defaultsTo: true, help: 'If necessary, build the app before running.')
      ..addOption('project-root', hide: !verboseHelp, help: 'Specify the project root directory.')
      ..addFlag(
        'hot',
        defaultsTo: kHotReloadDefault,
        help:
            'Run with support for hot reloading. Only available for debug mode. Not available with "--trace-startup".',
      )
      ..addFlag(
        'resident',
        defaultsTo: true,
        hide: !verboseHelp,
        help:
            'Stay resident after launching the application. Not available with "--trace-startup".',
      )
      ..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. '
            'The file is created when the signal handlers '
            'are hooked and deleted when they are removed.',
      )
      ..addFlag(
        'report-ready',
        help:
            'Print "ready" to the console after handling a keyboard command.\n'
            'This is primarily useful for tests and other automation, but consider '
            'using "--machine" instead.',
        hide: !verboseHelp,
      )
      ..addFlag(
        'benchmark',
        negatable: false,
        hide: !verboseHelp,
        help:
            'Enable a benchmarking mode. This will run the given application, '
            'measure the startup time and the app restart time, write the '
            'results out to "refresh_benchmark.json", and exit. This flag is '
            'intended for use in generating automated flutter benchmarks.',
      )
      // TODO(zanderso): Off by default with investigating whether this
      // is slower for certain use cases.
      // See: https://github.com/flutter/flutter/issues/49499
      ..addFlag(
        'fast-start',
        help:
            'Whether to quickly bootstrap applications with a minimal app. '
            'Currently this is only supported on Android devices. This option '
            'cannot be paired with "--${FlutterOptions.kUseApplicationBinary}".',
        hide: !verboseHelp,
      );
  }

  @override
  final name = 'run';

  @override
  DeprecationBehavior get deprecationBehavior =>
      boolArg('ignore-deprecation') ? DeprecationBehavior.ignore : _deviceDeprecationBehavior;
  DeprecationBehavior _deviceDeprecationBehavior = DeprecationBehavior.none;

  @override
  final description = 'Run your Flutter app on an attached device.';

  @override
  String get category => FlutterCommandCategory.project;

  List<Device>? devices;
  var webMode = false;

  String? get userIdentifier => stringArg(FlutterOptions.kDeviceUser);

  @override
  bool get startPausedDefault => false;

  @override
  Future<String?> get usagePath async {
    final String? command = await super.usagePath;

    if (devices == null) {
      return command;
    }
    if (devices!.length > 1) {
      return '$command/all';
    }
    return '$command/${getNameForTargetPlatform(await devices![0].targetPlatform)}';
  }

  @override
  Future<analytics.Event> unifiedAnalyticsUsageValues(String commandPath) async {
    final AnalyticsUsageValuesRecord record = await _sharedAnalyticsUsageValues;

    return analytics.Event.commandUsageValues(
      workflow: commandPath,
      commandHasTerminal: hasTerminal,
      runIsEmulator: record.runIsEmulator,
      runTargetName: record.runTargetName,
      runTargetOsVersion: record.runTargetOsVersion,
      runModeName: record.runModeName,
      runProjectModule: record.runProjectModule,
      runProjectHostLanguage: record.runProjectHostLanguage,
      runAndroidEmbeddingVersion: record.runAndroidEmbeddingVersion,
      runEnableImpeller: record.runEnableImpeller,
      runIOSInterfaceType: record.runIOSInterfaceType,
      runIsTest: record.runIsTest,
    );
  }

  late final Future<AnalyticsUsageValuesRecord> _sharedAnalyticsUsageValues = (() async {
    String deviceType, deviceOsVersion;
    bool isEmulator;
    var anyAndroidDevices = false;
    var anyIOSDevices = false;
    var anyWirelessIOSDevices = false;

    if (devices == null || devices!.isEmpty) {
      deviceType = 'none';
      deviceOsVersion = 'none';
      isEmulator = false;
    } else if (devices!.length == 1) {
      final Device device = devices![0];
      final TargetPlatform platform = await device.targetPlatform;
      anyAndroidDevices = platform == TargetPlatform.android;
      anyIOSDevices = platform == TargetPlatform.ios;
      if (device is IOSDevice && device.isWirelesslyConnected) {
        anyWirelessIOSDevices = true;
      }
      deviceType = getNameForTargetPlatform(platform);
      deviceOsVersion = await device.sdkNameAndVersion;
      isEmulator = await device.isLocalEmulator;
    } else {
      deviceType = 'multiple';
      deviceOsVersion = 'multiple';
      isEmulator = false;
      for (final Device device in devices!) {
        final TargetPlatform platform = await device.targetPlatform;
        anyAndroidDevices = anyAndroidDevices || (platform == TargetPlatform.android);
        anyIOSDevices = anyIOSDevices || (platform == TargetPlatform.ios);
        if (device is IOSDevice && device.isWirelesslyConnected) {
          anyWirelessIOSDevices = true;
        }
        if (anyAndroidDevices && anyIOSDevices) {
          break;
        }
      }
    }

    String? iOSInterfaceType;
    if (anyIOSDevices) {
      iOSInterfaceType = anyWirelessIOSDevices ? 'wireless' : 'usb';
    }

    String? androidEmbeddingVersion;
    final hostLanguage = <String>[];
    if (anyAndroidDevices) {
      final AndroidProject androidProject = FlutterProject.current().android;
      if (androidProject.existsSync()) {
        hostLanguage.add(androidProject.isKotlin ? 'kotlin' : 'java');
        androidEmbeddingVersion = androidProject.getEmbeddingVersion().toString().split('.').last;
      }
    }
    if (anyIOSDevices) {
      final IosProject iosProject = FlutterProject.current().ios;
      if (iosProject.exists) {
        final Iterable<File> swiftFiles = iosProject.hostAppRoot
            .listSync(recursive: true, followLinks: false)
            .whereType<File>()
            .where((File file) => globals.fs.path.extension(file.path) == '.swift');
        hostLanguage.add(swiftFiles.isNotEmpty ? 'swift' : 'objc');
      }
    }

    final BuildInfo buildInfo = await getBuildInfo();
    final String modeName = buildInfo.modeName;
    return (
      runIsEmulator: isEmulator,
      runTargetName: deviceType,
      runTargetOsVersion: deviceOsVersion,
      runModeName: modeName,
      runProjectModule: project.isModule,
      runProjectHostLanguage: hostLanguage.join(','),
      runAndroidEmbeddingVersion: androidEmbeddingVersion,
      runEnableImpeller: enableImpeller.asBool,
      runIOSInterfaceType: iOSInterfaceType,
      runIsTest: targetFile.endsWith('_test.dart'),
    );
  })();

  @override
  bool get shouldRunPub {
    // If we are running with a prebuilt application, do not run pub.
    if (runningWithPrebuiltApplication) {
      return false;
    }

    return super.shouldRunPub;
  }

  bool shouldUseHotMode(BuildInfo buildInfo) {
    final bool hotArg = boolArg('hot');
    final bool shouldUseHotMode = hotArg && !traceStartup;
    return buildInfo.isDebug && shouldUseHotMode;
  }

  bool get stayResident => boolArg('resident');
  bool get awaitFirstFrameWhenTracing => boolArg('await-first-frame-when-tracing');

  @override
  Future<void> validateCommand() async {
    // When running with a prebuilt application, no command validation is
    // necessary.
    if (!runningWithPrebuiltApplication) {
      await super.validateCommand();
    }

    devices = await findAllTargetDevices();
    if (devices == null) {
      throwToolExit(null);
    }
    if (globals.deviceManager!.hasSpecifiedAllDevices && runningWithPrebuiltApplication) {
      throwToolExit(
        'Using "-d all" with "--${FlutterOptions.kUseApplicationBinary}" is not supported',
      );
    }

    if (userIdentifier != null &&
        devices!.every((Device device) => device.platformType != PlatformType.android)) {
      throwToolExit(
        '--${FlutterOptions.kDeviceUser} is only supported for Android. At least one Android device is required.',
      );
    }

    if (devices!.any((Device device) => device is AndroidDevice)) {
      _deviceDeprecationBehavior = DeprecationBehavior.exit;
    }

    // Only support "web mode" with a single web device due to resident runner
    // refactoring required otherwise.
    webMode =
        featureFlags.isWebEnabled &&
        devices!.length == 1 &&
        await devices!.single.targetPlatform == TargetPlatform.web_javascript;

    if (useWasm && !webMode) {
      throwToolExit('--wasm is only supported on the web platform');
    }

    if (webRenderer == WebRendererMode.skwasm && !useWasm) {
      throwToolExit('Skwasm renderer requires --wasm');
    }

    final String? flavor = stringArg('flavor');
    final bool flavorsSupportedOnEveryDevice = devices!.every(
      (Device device) => device.supportsFlavors,
    );
    if (flavor != null && !flavorsSupportedOnEveryDevice) {
      globals.printWarning(
        '--flavor is only supported for Android, macOS, and iOS devices. '
        'Flavor-related features may not function properly and could '
        'behave differently in a future release.',
      );
    }
  }

  @visibleForTesting
  Future<ResidentRunner> createRunner({
    required bool hotMode,
    required List<FlutterDevice> flutterDevices,
    required String? applicationBinaryPath,
    required FlutterProject flutterProject,
  }) async {
    if (hotMode && !webMode) {
      return HotRunner(
        flutterDevices,
        target: targetFile,
        debuggingOptions: await createDebuggingOptions(webMode),
        benchmarkMode: boolArg('benchmark'),
        applicationBinary: applicationBinaryPath == null
            ? null
            : globals.fs.file(applicationBinaryPath),
        projectRootPath: stringArg('project-root'),
        dillOutputPath: stringArg('output-dill'),
        stayResident: stayResident,
        analytics: globals.analytics,
        nativeAssetsYamlFile: stringArg(FlutterOptions.kNativeAssetsYamlFile),
      );
    } else if (webMode) {
      return webRunnerFactory!.createWebRunner(
        flutterDevices.single,
        target: targetFile,
        flutterProject: flutterProject,
        debuggingOptions: await createDebuggingOptions(webMode),
        stayResident: stayResident,
        fileSystem: globals.fs,
        analytics: globals.analytics,
        logger: globals.logger,
        terminal: globals.terminal,
        platform: globals.platform,
        outputPreferences: globals.outputPreferences,
        systemClock: globals.systemClock,
      );
    }
    return ColdRunner(
      flutterDevices,
      target: targetFile,
      debuggingOptions: await createDebuggingOptions(webMode),
      traceStartup: traceStartup,
      awaitFirstFrameWhenTracing: awaitFirstFrameWhenTracing,
      applicationBinary: applicationBinaryPath == null
          ? null
          : globals.fs.file(applicationBinaryPath),
      stayResident: stayResident,
    );
  }

  @visibleForTesting
  Daemon createMachineDaemon() {
    return Daemon.createMachineDaemon();
  }

  @override
  Future<FlutterCommandResult> runCommand() async {
    final BuildInfo buildInfo = await getBuildInfo();
    // Enable hot mode by default if `--no-hot` was not passed and we are in
    // debug mode.
    final bool hotMode = shouldUseHotMode(buildInfo);
    final String? applicationBinaryPath = stringArg(FlutterOptions.kUseApplicationBinary);

    if (outputMachineFormat) {
      if (devices!.length > 1) {
        throwToolExit('"--machine" does not support "-d all".');
      }
      final Daemon daemon = createMachineDaemon();
      late AppInstance app;
      try {
        app = await daemon.appDomain.startApp(
          devices!.first,
          globals.fs.currentDirectory.path,
          targetFile,
          route,
          await createDebuggingOptions(webMode),
          hotMode,
          applicationBinary: applicationBinaryPath == null
              ? null
              : globals.fs.file(applicationBinaryPath),
          trackWidgetCreation: trackWidgetCreation,
          projectRootPath: stringArg('project-root'),
          packagesFilePath: globalResults![FlutterGlobalOptions.kPackagesOption] as String?,
          dillOutputPath: stringArg('output-dill'),
          userIdentifier: userIdentifier,
        );
      } on Exception catch (error) {
        throwToolExit(error.toString());
      }
      final DateTime appStartedTime = globals.systemClock.now();
      final int result = await app.runner.waitForAppToFinish();
      if (result != 0) {
        throwToolExit(null, exitCode: result);
      }
      return FlutterCommandResult(
        ExitStatus.success,
        timingLabelParts: <String>['daemon'],
        endTimeOverride: appStartedTime,
      );
    }
    globals.terminal.usesTerminalUi = true;

    final BuildMode buildMode = getBuildMode();
    for (final Device device in devices!) {
      if (!await device.supportsRuntimeMode(buildMode)) {
        throwToolExit(
          '${buildMode.uppercaseFriendlyName}'
          'mode is not supported by ${device.displayName}.',
        );
      }
      if (hotMode) {
        if (!device.supportsHotReload) {
          throwToolExit(
            'Hot reload is not supported by ${device.displayName}. '
            'Run with "--no-hot".',
          );
        }
      }
    }

    List<String>? expFlags;
    if (argParser.options.containsKey(FlutterOptions.kEnableExperiment) &&
        stringsArg(FlutterOptions.kEnableExperiment).isNotEmpty) {
      expFlags = stringsArg(FlutterOptions.kEnableExperiment);
    }
    final flutterDevices = <FlutterDevice>[
      for (final Device device in devices!)
        await FlutterDevice.create(
          device,
          experimentalFlags: expFlags,
          target: targetFile,
          buildInfo: buildInfo,
          userIdentifier: userIdentifier,
          platform: globals.platform,
        ),
    ];

    final ResidentRunner runner = await createRunner(
      applicationBinaryPath: applicationBinaryPath,
      flutterDevices: flutterDevices,
      flutterProject: project,
      hotMode: hotMode,
    );

    DateTime? appStartedTime;
    // Sync completer so the completing agent attaching to the resident doesn't
    // need to know about analytics.
    //
    // Do not add more operations to the future.
    final appStartedTimeRecorder = Completer<void>.sync();

    TerminalHandler? handler;
    // This callback can't throw.
    unawaited(
      appStartedTimeRecorder.future.then<void>((_) {
        appStartedTime = globals.systemClock.now();
        if (stayResident) {
          handler =
              TerminalHandler(
                  runner,
                  logger: globals.logger,
                  terminal: globals.terminal,
                  signals: globals.signals,
                  processInfo: globals.processInfo,
                  reportReady: boolArg('report-ready'),
                  pidFile: stringArg('pid-file'),
                )
                ..registerSignalHandlers()
                ..setupTerminal();
        }
      }),
    );
    try {
      final int? result = await runner.run(
        appStartedCompleter: appStartedTimeRecorder,
        route: route,
      );
      handler?.stop();
      if (result != 0) {
        throwToolExit(null, exitCode: result);
      }
    } on RPCError catch (error) {
      if (error.code == RPCErrorKind.kServiceDisappeared.code ||
          error.code == RPCErrorKind.kConnectionDisposed.code ||
          error.message.contains('Service connection disposed')) {
        throwToolExit('Lost connection to device.');
      }
      rethrow;
    } finally {
      // However we exited from the runner, ensure the terminal has line mode
      // and echo mode enabled before we return the user to the shell.
      try {
        globals.terminal.singleCharMode = false;
      } on StdinException {
        // Do nothing, if the STDIN handle is no longer available, there is nothing actionable for us to do at this point
      }
    }
    return FlutterCommandResult(
      ExitStatus.success,
      timingLabelParts: <String?>[
        if (hotMode) 'hot' else 'cold',
        getBuildMode().cliName,
        if (devices!.length == 1)
          getNameForTargetPlatform(await devices![0].targetPlatform)
        else
          'multiple',
        if (devices!.length == 1 && await devices![0].isLocalEmulator) 'emulator' else null,
      ],
      endTimeOverride: appStartedTime,
    );
  }
}

/// Schema for the usage values to send for analytics reporting.
typedef AnalyticsUsageValuesRecord = ({
  String? runAndroidEmbeddingVersion,
  bool? runEnableImpeller,
  String? runIOSInterfaceType,
  bool runIsEmulator,
  bool runIsTest,
  String runModeName,
  String runProjectHostLanguage,
  bool runProjectModule,
  String runTargetName,
  String runTargetOsVersion,
});
