// Copyright (c) 2014, 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';
import 'dart:isolate';
import 'dart:math';

import 'package:analysis_server/protocol/protocol_constants.dart'
    show PROTOCOL_VERSION;
import 'package:analysis_server/src/analytics/analytics_manager.dart';
import 'package:analysis_server/src/legacy_analysis_server.dart';
import 'package:analysis_server/src/lsp/lsp_socket_server.dart';
import 'package:analysis_server/src/server/crash_reporting.dart';
import 'package:analysis_server/src/server/crash_reporting_attachments.dart';
import 'package:analysis_server/src/server/detachable_filesystem_manager.dart';
import 'package:analysis_server/src/server/dev_server.dart';
import 'package:analysis_server/src/server/diagnostic_server.dart';
import 'package:analysis_server/src/server/error_notifier.dart';
import 'package:analysis_server/src/server/features.dart';
import 'package:analysis_server/src/server/http_server.dart';
import 'package:analysis_server/src/server/isolate_analysis_server.dart';
import 'package:analysis_server/src/server/lsp_stdio_server.dart';
import 'package:analysis_server/src/server/sdk_configuration.dart';
import 'package:analysis_server/src/server/stdio_server.dart';
import 'package:analysis_server/src/services/correction/assist_internal.dart';
import 'package:analysis_server/src/services/correction/fix_internal.dart';
import 'package:analysis_server/src/services/perf_witness/perf_witness.dart';
import 'package:analysis_server/src/session_logger/session_logger.dart';
import 'package:analysis_server/src/socket_server.dart';
import 'package:analysis_server/src/status/performance_logger.dart';
import 'package:analysis_server/src/utilities/env.dart' as env;
import 'package:analysis_server/src/utilities/request_statistics.dart';
import 'package:analysis_server/starter.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/instrumentation/file_instrumentation.dart';
import 'package:analyzer/instrumentation/instrumentation.dart';
import 'package:analyzer/src/dart/sdk/sdk.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:analyzer/src/util/sdk.dart';
import 'package:args/args.dart';
import 'package:linter/src/rules.dart' as linter;
import 'package:telemetry/crash_reporting.dart';
import 'package:unified_analytics/unified_analytics.dart';

import '../utilities/usage_tracking/usage_tracking.dart';

/// The [Driver] class represents a single running instance of the analysis
/// server application.  It is responsible for parsing command line options
/// and starting the HTTP and/or stdio servers.
class Driver implements ServerStarter {
  /// The name of the application that is used to start a server.
  static const binaryName = 'analysis_server';

  /// The name of the option used to set the identifier for the client.
  static const String clientIdOption = 'client-id';

  /// The name of the option used to set the version for the client.
  static const String clientVersionOption = 'client-version';

  /// The name of the option used to disable exception handling.
  static const String disableServerExceptionHandlingOption =
      'disable-server-exception-handling';

  /// The name of the option to disable the completion feature.
  static const String disableServerFeatureCompletionOption =
      'disable-server-feature-completion';

  /// The name of the option to disable the search feature.
  static const String disableServerFeatureSearchOption =
      'disable-server-feature-search';

  /// The name of the option to disable the debouncing of `server.status`
  /// notifications.
  static const String disableStatusNotificationDebouncingOption =
      'disable-status-notification-debouncing';

  /// The name of the option to prevent exceptions during analysis from being
  /// silent.
  static const String disableSilentAnalysisExceptionsOption =
      'disable-silent-analysis-exceptions';

  /// The name of the multi-option to enable one or more experiments.
  static const String enableExperimentOption = 'enable-experiment';

  /// The name of the option used to print usage information.
  static const String helpFlag = 'help';

  /// The name of the flag used to configure reporting legacy analytics.
  static const String analyticsFlag = 'analytics';

  /// The name of the flag used to suppress legacy analytics for this session.
  static const String suppressAnalyticsFlag = 'suppress-analytics';

  /// The name of the option used to cause instrumentation to also be written to
  /// a local file.
  static const String protocolTrafficLogOption = 'protocol-traffic-log';
  static const String protocolTrafficLogAliasOption =
      'instrumentation-log-file';

  /// The name of the option used to cause a log of the session to be written to
  /// a file.
  static const String sessionLogOption = 'session-log';

  /// The name of the option used to cause selected data from the analysis
  /// server to be written to a file.
  static const String performanceLogOption = 'performance-log';

  /// The name of the option used to specify if [print] should print to the
  /// console instead of being intercepted.
  static const String internalPrintToConsoleOption =
      'internal-print-to-console';

  /// The name of the option used to describe the new analysis driver logger.
  static const String analysisDriverLogOption = 'analysis-driver-log';
  static const String analysisDriverLogAliasOption = 'new-analysis-driver-log';

  /// The name of the option for specifying the http diagnostic port.
  /// If specified, users can review server status and performance information
  /// by opening a web browser on `http://localhost:<port>`.
  static const String diagnosticPortOption = 'diagnostic-port';
  static const String diagnosticPortAliasOption = 'port';

  /// The name of the option used to specify the path to the SDK.
  static const String dartSdkOption = 'dart-sdk';
  static const String dartSdkAliasOption = 'sdk';

  /// The name of the option used to specify the path to the data cache.
  static const String cacheFolderOption = 'cache';

  /// The name of the option used to specify the path to the package config file
  /// override.
  static const String packagesFileOption = 'packages';

  /// The name of the option used to specify a forced protocol version that the
  /// server will report to the client.
  static const String reportProtocolVersionOption = 'report-protocol-version';

  /// The name of the option specifying the server protocol to use.
  static const String serverProtocolOption = 'protocol';
  static const String protocolAnalyzer = 'analyzer';
  static const String protocolLsp = 'lsp';

  /// The name of the flag to use the Language Server Protocol (LSP).
  static const String useLspFlag = 'lsp';

  /// The name of the flag used to specify a directory to analyze in order to
  /// train an analysis server snapshot.
  static const String trainUsingOption = 'train-using';

  /// Flag to not use a (Evicting)FileByteStore.
  static const String disableFileByteStoreOption = 'disable-file-byte-store';

  /// The name of the flag to enable fine-grained dependencies.
  static const String withFineDependenciesOption = 'with-fine-dependencies';

  static const String pluginsFlag = 'plugins';

  /// The builder for attachments that should be included into crash reports.
  CrashReportingAttachmentsBuilder crashReportingAttachmentsBuilder =
      CrashReportingAttachmentsBuilder.empty;

  /// An optional manager to handle file systems which may not always be
  /// available.
  DetachableFileSystemManager? detachableFileSystemManager;

  /// The instrumentation service that is to be used by the analysis server.
  late final InstrumentationService _instrumentationService;

  /// The session logger.
  late final SessionLogger _sessionLogger;

  /// Use the given command-line [arguments] to start this server.
  ///
  /// If [sendPort] is not null, assumes this is launched in an isolate and will
  /// connect to the original isolate via an isolate channel.
  @override
  void start(
    List<String> arguments, {
    SendPort? sendPort,
    bool defaultToLsp = false,
  }) async {
    await startPerfWitness();

    var sessionStartTime = DateTime.now();
    var parser = createArgParser(defaultToLsp: defaultToLsp);
    var results = parser.parse(arguments);

    var analysisServerOptions = AnalysisServerOptions();
    analysisServerOptions.newAnalysisDriverLog =
        results.option(analysisDriverLogOption) ??
        results.option(analysisDriverLogAliasOption);
    if (results.wasParsed(useLspFlag)) {
      analysisServerOptions.useLanguageServerProtocol = results.flag(
        useLspFlag,
      );
    } else {
      analysisServerOptions.useLanguageServerProtocol =
          results.option(serverProtocolOption) == protocolLsp;
    }
    // For clients that don't supply their own identifier, use a default based
    // on whether the server will run in LSP mode or not.
    var clientId =
        results.option(clientIdOption) ??
        (analysisServerOptions.useLanguageServerProtocol
            ? 'unknown.client.lsp'
            : 'unknown.client.classic');
    analysisServerOptions.clientId = clientId;
    analysisServerOptions.clientVersion = results.option(clientVersionOption);
    analysisServerOptions.cacheFolder = results.option(cacheFolderOption);
    analysisServerOptions.packagesFile = results.option(packagesFileOption);
    analysisServerOptions.reportProtocolVersion = results.option(
      reportProtocolVersionOption,
    );
    analysisServerOptions.withFineDependencies = results.flag(
      withFineDependenciesOption,
    );

    analysisServerOptions.enabledExperiments = results.multiOption(
      enableExperimentOption,
    );

    // Read in any per-SDK overrides specified in <sdk>/config/settings.json.
    var sdkConfig = SdkConfiguration.readFromSdk();
    analysisServerOptions.configurationOverrides = sdkConfig;

    analysisServerOptions.usePlugins = results.flag(pluginsFlag);

    var defaultSdkPath = _getSdkPath(results);
    var dartSdkManager = DartSdkManager(defaultSdkPath);

    // TODO(brianwilkerson): It would be nice to avoid creating an SDK that
    // can't be re-used, but the SDK is needed to create a package map provider
    // in the case where we need to run `pub` in order to get the package map.
    var defaultSdk = _createDefaultSdk(defaultSdkPath);

    // Create the analytics manager.

    var tool =
        env.topLevelTool ??
        switch (clientId) {
          'VS-Code' || 'VS-Code-Remote' => DashTool.vscodePlugins,
          'IntelliJ-IDEA' => DashTool.intellijPlugins,
          'Android-Studio' => DashTool.androidStudioPlugins,
          _ => null,
        };

    // Analytics (legacy, and unified).
    var disableAnalyticsForSession =
        tool == null ||
        areAnalyticsSuppressed() ||
        results.flag(suppressAnalyticsFlag) ||
        results.wasParsed(trainUsingOption);

    var analytics = disableAnalyticsForSession
        ? NoOpAnalytics()
        : _createAnalytics(defaultSdk, defaultSdkPath, tool);
    var analyticsManager = AnalyticsManager(analytics);

    bool shouldSendCallback() {
      // Check sdkConfig to optionally force reporting on.
      if (sdkConfig.crashReportingForceEnabled == true) {
        return true;
      }

      // Reuse the unified_analytics consent mechanism to determine whether
      // we can send a crash report.
      return analyticsManager.analytics.okToSend;
    }

    // Crash reporting

    // Use sdkConfig to optionally override analytics settings.
    var crashProductId = sdkConfig.crashReportingId ?? 'Dart_analysis_server';
    var crashReportSender = CrashReportSender.prod(
      crashProductId,
      shouldSendCallback,
    );

    {
      var disableCompletion = results.flag(
        disableServerFeatureCompletionOption,
      );
      var disableSearch = results.flag(disableServerFeatureSearchOption);
      if (disableCompletion || disableSearch) {
        analysisServerOptions.featureSet = FeatureSet(
          completion: !disableCompletion,
          search: !disableSearch,
        );
      }
    }

    if (results.flag(helpFlag)) {
      _printUsage(parser, fromHelp: true);
      return;
    }

    // Record the start of the session.
    analyticsManager.startUp(
      time: sessionStartTime,
      arguments: _getArgumentsForAnalytics(results),
      clientId: clientId,
      clientVersion: analysisServerOptions.clientVersion,
    );
    //
    // Initialize the instrumentation service.
    //
    var logFilePath =
        results.option(protocolTrafficLogOption) ??
        results.option(protocolTrafficLogAliasOption);
    var allInstrumentationServices = <InstrumentationService>[];
    if (logFilePath != null) {
      _rollLogFiles(logFilePath, 5);
      allInstrumentationServices.add(
        InstrumentationLogAdapter(
          FileInstrumentationLogger(logFilePath),
          watchEventExclusionFiles: {logFilePath},
        ),
      );
    }

    var errorNotifier = ErrorNotifier();
    allInstrumentationServices.add(errorNotifier);
    allInstrumentationServices.add(
      CrashReportingInstrumentation(crashReportSender),
    );
    _instrumentationService = MulticastInstrumentationService(
      allInstrumentationServices,
    );

    _instrumentationService.logVersion(
      results.option(trainUsingOption) != null
          ? 'training-0'
          : _readUuid(_instrumentationService),
      analysisServerOptions.clientId ?? '',
      analysisServerOptions.clientVersion ?? '',
      PROTOCOL_VERSION,
      defaultSdk.languageVersion.toString(),
    );
    AnalysisEngine.instance.instrumentationService = _instrumentationService;

    // Initialize the session logging service.
    var sessionLogFilePath = results.option(sessionLogOption);
    _sessionLogger = SessionLogger(filePath: sessionLogFilePath);
    _sessionLogger.normalizer.addReplacementsForPath(
      defaultSdkPath,
      'dartSdkRoot',
    );
    _sessionLogger.logCommandLine(arguments: arguments);

    int? diagnosticServerPort;
    var portValue =
        results.option(diagnosticPortOption) ??
        results.option(diagnosticPortAliasOption);
    if (portValue != null) {
      try {
        diagnosticServerPort = int.parse(portValue);
      } on FormatException {
        print('Invalid port number: $portValue');
        print('');
        _printUsage(parser);
        exitCode = 1;
        return;
      }
    }

    PerformanceLogger? performanceLogger;
    var performanceLogFilePath = results.option(performanceLogOption);
    if (performanceLogFilePath != null) {
      _rollLogFiles(performanceLogFilePath, 5);
      performanceLogger = PerformanceLogger(performanceLogFilePath);
    }

    // TODO(brianwilkerson): Pass the following value to the server and
    // implement the debouncing when it hasn't been disabled.
    // var disableDebouncing = results[DISABLE_STATUS_NOTIFICATION_DEBOUNCING] as bool;

    var environment = env.map(
      tool: tool,
      suppressAnalytics: disableAnalyticsForSession,
    );

    if (analysisServerOptions.useLanguageServerProtocol) {
      if (sendPort != null) {
        throw UnimplementedError(
          'Isolate usage not implemented for LspAnalysisServer',
        );
      }
      startLspServer(
        results,
        analysisServerOptions,
        dartSdkManager,
        analyticsManager,
        _instrumentationService,
        _sessionLogger,
        diagnosticServerPort,
        errorNotifier,
        performanceLogger,
        environment,
      );
    } else {
      startAnalysisServer(
        results,
        analysisServerOptions,
        parser,
        dartSdkManager,
        analyticsManager,
        crashReportingAttachmentsBuilder,
        _instrumentationService,
        _sessionLogger,
        RequestStatisticsHelper(),
        diagnosticServerPort,
        errorNotifier,
        sendPort,
        performanceLogger,
        environment,
      );
    }

    configureMemoryUsageTracking(
      arguments,
      (memoryUsageEvent) => analyticsManager.sendMemoryUsage(memoryUsageEvent),
    );
  }

  void startAnalysisServer(
    ArgResults results,
    AnalysisServerOptions analysisServerOptions,
    ArgParser parser,
    DartSdkManager dartSdkManager,
    AnalyticsManager analyticsManager,
    CrashReportingAttachmentsBuilder crashReportingAttachmentsBuilder,
    InstrumentationService instrumentationService,
    SessionLogger sessionLogger,
    RequestStatisticsHelper requestStatistics,
    int? diagnosticServerPort,
    ErrorNotifier errorNotifier,
    SendPort? sendPort,
    PerformanceLogger? performanceLogger,
    Map<String, String>? environment,
  ) {
    var capture = results.flag(disableServerExceptionHandlingOption)
        ? (_, Function f, {void Function(String)? print}) => f()
        : _captureExceptions;
    var trainDirectory = results.option(trainUsingOption);
    if (trainDirectory != null) {
      if (!FileSystemEntity.isDirectorySync(trainDirectory)) {
        print("Training directory '$trainDirectory' not found.\n");
        exitCode = 1;
        return;
      }
    }
    linter.registerLintRules();
    registerBuiltInAssistGenerators();
    registerBuiltInFixGenerators();

    var diagnosticServer = _DiagnosticServerImpl();

    //
    // Create the sockets and start listening for requests.
    //
    var socketServer = SocketServer(
      analysisServerOptions,
      dartSdkManager,
      crashReportingAttachmentsBuilder,
      instrumentationService,
      sessionLogger,
      requestStatistics,
      diagnosticServer,
      analyticsManager,
      detachableFileSystemManager,
      performanceLogger,
      environment: environment,
    );

    diagnosticServer.httpServer = HttpAnalysisServer(socketServer);
    if (diagnosticServerPort != null) {
      diagnosticServer.startOnPort(diagnosticServerPort);
    }

    if (trainDirectory != null) {
      if (sendPort != null) {
        throw UnimplementedError(
          'isolate usage not supported for DevAnalysisServer',
        );
      }
      var tempDriverDir = Directory.systemTemp.createTempSync(
        'analysis_server_',
      );
      analysisServerOptions.cacheFolder = tempDriverDir.path;
      analysisServerOptions.disableFileByteStore = results.flag(
        disableFileByteStoreOption,
      );

      var devServer = DevAnalysisServer(socketServer);
      devServer.initServer();

      () async {
        // We first analyze code with an empty driver cache.
        print('Analyzing with an empty driver cache:');
        var exitCode = await devServer.processDirectories([trainDirectory]);
        if (exitCode != 0) exit(exitCode);

        print('');

        // Then again with a populated cache.
        print('Analyzing with a populated driver cache:');
        exitCode = await devServer.processDirectories([trainDirectory]);
        if (exitCode != 0) exit(exitCode);

        diagnosticServer.httpServer.close();
        await instrumentationService.shutdown();
        unawaited(socketServer.analysisServer!.shutdown());

        try {
          tempDriverDir.deleteSync(recursive: true);
        } catch (_) {
          // Ignore any exception.
        }

        exit(exitCode);
      }();
    } else {
      capture(
        instrumentationService,
        () {
          Future<void> serveResult;
          if (sendPort == null) {
            var stdioServer = StdioAnalysisServer(socketServer);
            serveResult = stdioServer.serveStdio();
          } else {
            var isolateAnalysisServer = IsolateAnalysisServer(socketServer);
            serveResult = isolateAnalysisServer.serveIsolate(sendPort);
          }
          errorNotifier.server = socketServer.analysisServer;
          if (results.flag(disableSilentAnalysisExceptionsOption)) {
            errorNotifier.sendSilentExceptionsToClient = true;
          }
          serveResult.then((_) async {
            diagnosticServer.httpServer.close();
            await instrumentationService.shutdown();
            unawaited(socketServer.analysisServer!.shutdown());
            if (sendPort == null) exit(0);
          });
        },
        print: results.flag(internalPrintToConsoleOption)
            ? null
            : diagnosticServer.httpServer.recordPrint,
      );
    }
  }

  void startLspServer(
    ArgResults args,
    AnalysisServerOptions analysisServerOptions,
    DartSdkManager dartSdkManager,
    AnalyticsManager analyticsManager,
    InstrumentationService instrumentationService,
    SessionLogger sessionLogger,
    int? diagnosticServerPort,
    ErrorNotifier errorNotifier,
    PerformanceLogger? performanceLogger,
    Map<String, String>? environment,
  ) {
    var capture = args.flag(disableServerExceptionHandlingOption)
        ? (_, Function f, {void Function(String)? print}) => f()
        : _captureExceptions;

    linter.registerLintRules();
    registerBuiltInAssistGenerators();
    registerBuiltInFixGenerators();

    var diagnosticServer = _DiagnosticServerImpl();

    var socketServer = LspSocketServer(
      analysisServerOptions,
      diagnosticServer,
      analyticsManager,
      dartSdkManager,
      instrumentationService,
      sessionLogger,
      detachableFileSystemManager,
      performanceLogger,
      environment: environment,
    );
    errorNotifier.server = socketServer.analysisServer;
    diagnosticServer.httpServer = HttpAnalysisServer(socketServer);

    if (diagnosticServerPort != null) {
      diagnosticServer.startOnPort(diagnosticServerPort);
    }

    capture(instrumentationService, () {
      var stdioServer = LspStdioAnalysisServer(socketServer);
      stdioServer.serveStdio().then((_) {
        // Only shutdown the server and exit if the server is not already
        // handling the shutdown.
        if (!socketServer.analysisServer!.willExit) {
          unawaited(socketServer.analysisServer!.shutdown());
          exit(0);
        }
      });
    });
  }

  /// Execute the given [callback] within a zone that will capture any unhandled
  /// exceptions and both report them to the client and send them to the given
  /// instrumentation [service]. If a [print] function is provided, then also
  /// capture any data printed by the callback and redirect it to the function.
  void _captureExceptions(
    InstrumentationService service,
    void Function() callback, {
    void Function(String line)? print,
  }) {
    void errorFunction(
      Zone self,
      ZoneDelegate parent,
      Zone zone,
      Object exception,
      StackTrace stackTrace,
    ) {
      service.logException(exception, stackTrace);
      throw exception;
    }

    var printFunction = print == null
        ? null
        : (Zone self, ZoneDelegate parent, Zone zone, String line) {
            // Note: we don't pass the line on to stdout, because that is
            // reserved for communication to the client.
            print(line);
          };
    var zoneSpecification = ZoneSpecification(
      handleUncaughtError: errorFunction,
      print: printFunction,
    );
    return runZoned(callback, zoneSpecification: zoneSpecification);
  }

  /// Create the `Analytics` instance to be used to report analytics.
  Analytics _createAnalytics(
    DartSdk dartSdk,
    String dartSdkPath,
    DashTool tool,
  ) {
    // TODO(brianwilkerson): Find out whether there's a way to get the channel
    //  without running `flutter channel`.
    var pathContext = PhysicalResourceProvider.INSTANCE.pathContext;
    var flutterSdkRoot = pathContext.dirname(
      pathContext.dirname(pathContext.dirname(dartSdkPath)),
    );
    var flutterVersionFile = PhysicalResourceProvider.INSTANCE.getFile(
      pathContext.join(flutterSdkRoot, 'version'),
    );
    String? flutterVersion;
    try {
      flutterVersion = flutterVersionFile.readAsStringSync();
    } catch (exception) {
      // If we can't read the file, ignore it.
    }
    return Analytics(
      tool: tool,
      dartVersion: dartSdk.sdkVersion,
      // flutterChannel: '',
      flutterVersion: flutterVersion,
    );
  }

  DartSdk _createDefaultSdk(String defaultSdkPath) {
    var resourceProvider = PhysicalResourceProvider.INSTANCE;
    return FolderBasedDartSdk(
      resourceProvider,
      resourceProvider.getFolder(defaultSdkPath),
    );
  }

  /// Constructs a uuid combining the current date and a random integer.
  String _generateUuidString() {
    var millisecondsSinceEpoch = DateTime.now().millisecondsSinceEpoch;
    var random = Random().nextInt(0x3fffffff);
    return '$millisecondsSinceEpoch$random';
  }

  /// Return a list of the known command-line arguments that were parsed when
  /// creating the given [results]. We return only the names of the arguments,
  /// not the values of the arguments, in order to prevent the collection of any
  /// PII.
  List<String> _getArgumentsForAnalytics(ArgResults results) {
    // The arguments that are literal strings are deprecated and no longer read
    // from, but we still want to know if clients are using them.
    var knownArguments = [
      analyticsFlag,
      cacheFolderOption,
      clientIdOption,
      clientVersionOption,
      'completion-model',
      'dartpad',
      dartSdkOption,
      dartSdkAliasOption,
      diagnosticPortOption,
      diagnosticPortAliasOption,
      disableServerExceptionHandlingOption,
      disableServerFeatureCompletionOption,
      disableServerFeatureSearchOption,
      disableSilentAnalysisExceptionsOption,
      disableStatusNotificationDebouncingOption,
      'enable-completion-model',
      'enable-experiment',
      'enable-instrumentation',
      'file-read-mode',
      helpFlag,
      'ignore-unrecognized-flags',
      internalPrintToConsoleOption,
      packagesFileOption,
      'preview-dart-2',
      protocolTrafficLogOption,
      protocolTrafficLogAliasOption,
      reportProtocolVersionOption,
      serverProtocolOption,
      sessionLogOption,
      suppressAnalyticsFlag,
      trainUsingOption,
      'useAnalysisHighlight2',
      useLspFlag,
      'use-new-relevance',
      'use-fasta-parser',
      disableFileByteStoreOption,
      withFineDependenciesOption,
    ];
    return knownArguments
        .where((argument) => results.wasParsed(argument))
        .toList();
  }

  String _getSdkPath(ArgResults args) {
    String? sdkPath;

    void tryCandidateArgument(String argumentName) {
      var argumentValue = args[argumentName];
      if (sdkPath == null && argumentValue is String) {
        sdkPath = argumentValue;
      }
    }

    tryCandidateArgument(dartSdkOption);
    tryCandidateArgument(dartSdkAliasOption);
    var sdkPath2 = sdkPath ?? getSdkPath();

    var pathContext = PhysicalResourceProvider.INSTANCE.pathContext;
    return pathContext.normalize(pathContext.absolute(sdkPath2));
  }

  /// Print information about how to use the server.
  void _printUsage(ArgParser parser, {bool fromHelp = false}) {
    print('Usage: $binaryName [flags]');
    print('');
    print('Supported flags are:');
    print(parser.usage);
  }

  /// Read the UUID from disk, generating and storing a new one if necessary.
  String _readUuid(InstrumentationService service) {
    var instrumentationLocation = PhysicalResourceProvider.INSTANCE
        .getStateLocation('.instrumentation');
    if (instrumentationLocation == null) {
      return _generateUuidString();
    }
    var uuidFile = File(instrumentationLocation.getChild('uuid.txt').path);
    try {
      if (uuidFile.existsSync()) {
        var uuid = uuidFile.readAsStringSync();
        if (uuid.length > 5) {
          return uuid;
        }
      }
    } catch (exception, stackTrace) {
      service.logException(exception, stackTrace);
    }
    var uuid = _generateUuidString();
    try {
      uuidFile.parent.createSync(recursive: true);
      uuidFile.writeAsStringSync(uuid);
    } catch (exception, stackTrace) {
      service.logException(exception, stackTrace);
      // Slightly alter the uuid to indicate it was not persisted
      uuid = 'temp-$uuid';
    }
    return uuid;
  }

  /// Create and return the parser used to parse the command-line arguments.
  static ArgParser createArgParser({
    int? usageLineLength,
    bool includeHelpFlag = true,
    bool defaultToLsp = false,
  }) {
    var parser = ArgParser(usageLineLength: usageLineLength);
    if (includeHelpFlag) {
      parser.addFlag(
        helpFlag,
        abbr: 'h',
        negatable: false,
        help: 'Print this usage information.',
      );
    }
    parser.addOption(
      clientIdOption,
      valueHelp: 'name',
      help: 'An identifier for the analysis server client.',
    );
    parser.addOption(
      clientVersionOption,
      valueHelp: 'version',
      help: 'The version of the analysis server client.',
    );
    parser.addOption(
      dartSdkOption,
      valueHelp: 'path',
      help: 'Override the Dart SDK to use for analysis.',
    );
    parser.addOption(dartSdkAliasOption, hide: true);
    parser.addOption(
      cacheFolderOption,
      valueHelp: 'path',
      help: 'Override the location of the analysis server\'s cache.',
    );
    parser.addOption(
      packagesFileOption,
      valueHelp: 'path',
      help:
          'The path to the package resolution configuration file, which '
          'supplies a mapping of package names\ninto paths.',
    );
    parser.addMultiOption(
      enableExperimentOption,
      valueHelp: 'experiment',
      help:
          'Enable one or more experimental features '
          '(see dart.dev/go/experiments).',
      hide: true,
    );

    parser.addOption(
      serverProtocolOption,
      defaultsTo: defaultToLsp ? protocolLsp : protocolAnalyzer,
      valueHelp: 'protocol',
      allowed: [protocolLsp, protocolAnalyzer],
      allowedHelp: {
        protocolLsp:
            'The Language Server Protocol '
            '(https://microsoft.github.io/language-server-protocol)',
        protocolAnalyzer:
            'Dart\'s analysis server protocol '
            '(https://dart.dev/go/analysis-server-protocol)',
      },
      help:
          'Specify the protocol to use to communicate with the analysis server.',
    );
    // This option is hidden but still accepted; it's effectively translated to
    // the 'protocol' option above.
    parser.addFlag(
      useLspFlag,
      negatable: false,
      help: 'Whether to use the Language Server Protocol (LSP).',
      hide: true,
    );

    parser.addSeparator('Server diagnostics:');

    parser.addOption(
      protocolTrafficLogOption,
      valueHelp: 'file path',
      help: 'Write server protocol traffic to the given file.',
    );
    parser.addOption(protocolTrafficLogAliasOption, hide: true);

    parser.addOption(
      sessionLogOption,
      valueHelp: 'file path',
      help: 'Write a server session log to the given file.',
    );

    parser.addOption(
      performanceLogOption,
      valueHelp: 'file path',
      help: 'Write a server performance log to the given file.',
    );

    parser.addOption(
      analysisDriverLogOption,
      valueHelp: 'file path',
      help: 'Write analysis driver diagnostic data to the given file.',
    );
    parser.addOption(analysisDriverLogAliasOption, hide: true);

    parser.addOption(
      diagnosticPortOption,
      valueHelp: 'port',
      help:
          'Serve a web UI for status and performance data on the given '
          'port.',
    );
    parser.addOption(diagnosticPortAliasOption, hide: true);

    //
    // Hidden; these have not yet been made public.
    //
    parser.addFlag(
      analyticsFlag,
      help:
          'Allow or disallow sending analytics information to '
          'Google for this session.',
      hide: true,
    );
    parser.addFlag(
      suppressAnalyticsFlag,
      negatable: false,
      help: 'Suppress analytics for this session.',
      hide: true,
    );

    //
    // Hidden; these are for internal development.
    //
    parser.addOption(
      trainUsingOption,
      valueHelp: 'path',
      help:
          'Pass in a directory to analyze for purposes of training an '
          'analysis server snapshot.  Disables analytics.',
      hide: true,
    );
    parser.addFlag(
      disableFileByteStoreOption,
      help:
          'Disable use of (Evicting)FileByteStore. Intended for benchmarking.',
      hide: true,
    );
    parser.addFlag(
      withFineDependenciesOption,
      help: 'Enable fine-grained dependencies.',
      defaultsTo: true,
      hide: true,
    );
    parser.addFlag(
      disableServerExceptionHandlingOption,
      // TODO(jcollins-g): Pipeline option through and apply to all
      // exception-nullifying runZoned() calls.
      help:
          'disable analyzer exception capture for interactive debugging '
          'of the server',
      hide: true,
    );
    parser.addFlag(
      disableServerFeatureCompletionOption,
      help: 'disable all completion features',
      hide: true,
    );
    parser.addFlag(
      disableServerFeatureSearchOption,
      help: 'disable all search features',
      hide: true,
    );
    parser.addFlag(
      pluginsFlag,
      help: 'Use analyzer plugins',
      defaultsTo: true,
      hide: true,
    );
    parser.addFlag(
      disableSilentAnalysisExceptionsOption,
      negatable: false,
      help: 'Prevent exceptions during analysis from being silent',
      hide: true,
    );
    parser.addFlag(
      disableStatusNotificationDebouncingOption,
      negatable: false,
      help: 'Suppress debouncing of status notifications.',
      hide: true,
    );
    parser.addFlag(
      internalPrintToConsoleOption,
      help: 'enable sending `print` output to the console',
      negatable: false,
      hide: true,
    );
    parser.addOption(
      reportProtocolVersionOption,
      valueHelp: 'version',
      help:
          'The protocol version that the server will report to the client, '
          'can be used to temporary enabling features that we expect to be '
          'available in future versions.',
      hide: true,
    );

    //
    // Hidden; these are deprecated and no longer read from.
    //

    // Removed 11/15/2020.
    parser.addOption('completion-model', hide: true);
    // Removed 11/8/2020.
    parser.addFlag('dartpad', hide: true);
    // Removed 11/15/2020.
    parser.addFlag('enable-completion-model', hide: true);
    // Removed 9/23/2020.
    parser.addFlag('enable-instrumentation', hide: true);
    // Removed 11/12/2020.
    parser.addOption('file-read-mode', hide: true);
    // Removed 11/12/2020.
    parser.addFlag('ignore-unrecognized-flags', hide: true);
    // Removed 11/8/2020.
    parser.addFlag('preview-dart-2', hide: true);
    // Removed 11/12/2020.
    parser.addFlag('useAnalysisHighlight2', hide: true);
    // Removed 11/13/2020.
    parser.addFlag('use-new-relevance', hide: true);
    // Removed 9/23/2020.
    parser.addFlag('use-fasta-parser', hide: true);

    return parser;
  }

  /// Perform log files rolling.
  ///
  /// Rename existing files with names `[path].(x)` to `[path].(x+1)`.
  /// Keep at most [numOld] files.
  /// Rename the file with the given [path] to `[path].1`.
  static void _rollLogFiles(String path, int numOld) {
    for (var i = numOld - 1; i >= 0; i--) {
      try {
        var oldPath = i == 0 ? path : '$path.$i';
        File(oldPath).renameSync('$path.${i + 1}');
      } catch (e) {
        // If a file can't be renamed, then leave it and attempt to rename the
        // remaining files.
      }
    }
  }
}

/// Implements the [DiagnosticServer] class by wrapping an [HttpAnalysisServer].
class _DiagnosticServerImpl extends DiagnosticServer {
  late HttpAnalysisServer httpServer;

  @override
  Future<int> getServerPort() async => (await httpServer.serveHttp())!;

  Future<void> startOnPort(int port) {
    return httpServer.serveHttp(port);
  }
}
