serveDevTools function

Future<HttpServer?> serveDevTools({
  1. bool enableStdinCommands = true,
  2. bool machineMode = false,
  3. bool debugMode = false,
  4. bool launchBrowser = false,
  5. bool enableNotifications = false,
  6. bool allowEmbedding = false,
  7. bool headlessMode = false,
  8. bool verboseMode = false,
  9. String? hostname,
  10. String? customDevToolsPath,
  11. int port = 0,
  12. int numPortsToTry = defaultTryPorts,
  13. Handler? handler,
  14. String? serviceProtocolUri,
  15. String? profileFilename,
  16. String? appSizeBase,
  17. String? appSizeTest,
})

Serves DevTools.

handler is the shelf.Handler that the server will use for all requests. If null, defaultHandler will be used. Defaults to null.

customDevToolsPath is a path to a directory containing a pre-built DevTools application. If not provided, the pre-built DevTools application shipped via pub will be used.

Implementation

// Note: this method is used by the Dart CLI, Flutter CLI, and by package:dwds.
Future<HttpServer?> serveDevTools({
  bool enableStdinCommands = true,
  bool machineMode = false,
  bool debugMode = false,
  bool launchBrowser = false,
  bool enableNotifications = false,
  bool allowEmbedding = false,
  bool headlessMode = false,
  bool verboseMode = false,
  String? hostname,
  String? customDevToolsPath,
  int port = 0,
  int numPortsToTry = defaultTryPorts,
  shelf.Handler? handler,
  String? serviceProtocolUri,
  String? profileFilename,
  String? appSizeBase,
  String? appSizeTest,
}) async {
  hostname ??= 'localhost';

  // Collect profiling information.
  if (profileFilename != null && serviceProtocolUri != null) {
    final Uri? vmServiceUri = Uri.tryParse(serviceProtocolUri);
    if (vmServiceUri != null) {
      await _hookupMemoryProfiling(vmServiceUri, profileFilename, verboseMode);
    }
    return null;
  }

  if (machineMode) {
    assert(enableStdinCommands,
        'machineMode only works with enableStdinCommands.');
  }

  clients = ClientManager(enableNotifications);

  handler ??= await defaultHandler(
    clients,
    customDevToolsPath: customDevToolsPath,
    debugMode: debugMode,
  );

  HttpServer? server;
  SocketException? ex;
  while (server == null && numPortsToTry >= 0) {
    // If we have tried [numPortsToTry] ports and still have not been able to
    // connect, try port 0 to find a random available port.
    if (numPortsToTry == 0) port = 0;

    try {
      server = await HttpMultiServer.bind(hostname, port);
    } on SocketException catch (e) {
      ex = e;
      numPortsToTry--;
      port++;
    }
  }

  // Re-throw the last exception if we failed to bind.
  if (server == null && ex != null) {
    throw ex;
  }

  final _server = server!;
  if (allowEmbedding) {
    _server.defaultResponseHeaders.remove('x-frame-options', 'SAMEORIGIN');
  }

  // Ensure browsers don't cache older versions of the app.
  _server.defaultResponseHeaders
      .add(HttpHeaders.cacheControlHeader, 'max-age=900');
  // Serve requests in an error zone to prevent failures
  // when running from another error zone.
  runZonedGuarded(() => shelf.serveRequests(_server, handler!), (e, _) {
    print('Error serving requests: $e');
  });

  final devToolsUrl = 'http://${_server.address.host}:${_server.port}';

  if (launchBrowser) {
    if (serviceProtocolUri != null) {
      serviceProtocolUri =
          _normalizeVmServiceUri(serviceProtocolUri).toString();
    }

    final queryParameters = {
      if (serviceProtocolUri != null) 'uri': serviceProtocolUri,
      if (appSizeBase != null) 'appSizeBase': appSizeBase,
      if (appSizeTest != null) 'appSizeTest': appSizeTest,
    };
    String url = Uri.parse(devToolsUrl)
        .replace(queryParameters: queryParameters)
        .toString();

    // If app size parameters are present, open to the standalone `appsize`
    // page, regardless if there is a vm service uri specified. We only check
    // for the presence of [appSizeBase] here because [appSizeTest] may or may
    // not be specified (it should only be present for diffs). If [appSizeTest]
    // is present without [appSizeBase], we will ignore the parameter.
    if (appSizeBase != null) {
      final startQueryParamIndex = url.indexOf('?');
      if (startQueryParamIndex != -1) {
        url = '${url.substring(0, startQueryParamIndex)}'
            '/#/appsize'
            '${url.substring(startQueryParamIndex)}';
      }
    }

    try {
      await Chrome.start([url]);
    } catch (e) {
      print('Unable to launch Chrome: $e\n');
    }
  }

  if (enableStdinCommands) {
    String message = 'Serving DevTools at $devToolsUrl.\n'
        '\n'
        'Hit ctrl-c to terminate the server.';
    if (!machineMode && debugMode) {
      // Add bold to help find the correct url to open.
      message = '\u001b[1m$message\u001b[0m\n';
    }

    printOutput(
      message,
      {
        'event': 'server.started',
        // TODO(dantup): Remove this `method` field when we're sure VS Code
        // users are all on a newer version that uses `event`. We incorrectly
        // used `method` for the original releases.
        'method': 'server.started',
        'params': {
          'host': _server.address.host,
          'port': _server.port,
          'pid': pid,
          'protocolVersion': protocolVersion,
        }
      },
      machineMode: machineMode,
    );

    // TODO: Refactor machine mode out into a separate class.
    if (machineMode) {
      final Stream<Map<String, dynamic>> _stdinCommandStream = stdin
          .transform<String>(utf8.decoder)
          .transform<String>(const LineSplitter())
          .where((String line) => line.startsWith('{') && line.endsWith('}'))
          .map<Map<String, dynamic>>((String line) {
        return json.decode(line) as Map<String, dynamic>;
      });

      // Example input:
      // {
      //   "id":0,
      //   "method":"vm.register",
      //   "params":{
      //     "uri":"<vm-service-uri-here>",
      //   }
      // }
      _stdinCommandStream.listen((Map<String, dynamic> json) async {
        // ID can be String, int or null
        final dynamic id = json['id'];
        final Map<String, dynamic> params = json['params'];

        switch (json['method']) {
          case 'vm.register':
            await _handleVmRegister(
              id,
              params,
              machineMode,
              headlessMode,
              devToolsUrl,
            );
            break;
          case 'devTools.launch':
            await _handleDevToolsLaunch(
              id,
              params,
              machineMode,
              headlessMode,
              devToolsUrl,
            );
            break;
          case 'client.list':
            await _handleClientsList(id, params, machineMode);
            break;
          case 'devTools.survey':
            _devToolsUsage ??= DevToolsUsage();
            final String surveyRequest = params['surveyRequest'];
            final String value = params['value'];
            switch (surveyRequest) {
              case 'copyAndCreateDevToolsFile':
                // Backup and delete ~/.devtools file.
                if (backupAndCreateDevToolsStore()) {
                  _devToolsUsage = null;
                  printOutput(
                    'DevTools Survey',
                    {
                      'id': id,
                      'result': {
                        'sucess': true,
                      },
                    },
                    machineMode: machineMode,
                  );
                }
                break;
              case 'restoreDevToolsFile':
                _devToolsUsage = null;
                final content = restoreDevToolsStore();
                if (content != null) {
                  printOutput(
                    'DevTools Survey',
                    {
                      'id': id,
                      'result': {
                        'sucess': true,
                        'content': content,
                      },
                    },
                    machineMode: machineMode,
                  );

                  _devToolsUsage = null;
                }
                break;
              case apiSetActiveSurvey:
                _devToolsUsage!.activeSurvey = value;
                printOutput(
                  'DevTools Survey',
                  {
                    'id': id,
                    'result': {
                      'sucess': _devToolsUsage!.activeSurvey == value,
                      'activeSurvey': _devToolsUsage!.activeSurvey,
                    },
                  },
                  machineMode: machineMode,
                );
                break;
              case apiGetSurveyActionTaken:
                printOutput(
                  'DevTools Survey',
                  {
                    'id': id,
                    'result': {
                      'activeSurvey': _devToolsUsage!.activeSurvey,
                      'surveyActionTaken': _devToolsUsage!.surveyActionTaken,
                    },
                  },
                  machineMode: machineMode,
                );
                break;
              case apiSetSurveyActionTaken:
                _devToolsUsage!.surveyActionTaken = jsonDecode(value);
                printOutput(
                  'DevTools Survey',
                  {
                    'id': id,
                    'result': {
                      'activeSurvey': _devToolsUsage!.activeSurvey,
                      'surveyActionTaken': _devToolsUsage!.surveyActionTaken,
                    },
                  },
                  machineMode: machineMode,
                );
                break;
              case apiGetSurveyShownCount:
                printOutput(
                  'DevTools Survey',
                  {
                    'id': id,
                    'result': {
                      'activeSurvey': _devToolsUsage!.activeSurvey,
                      'surveyShownCount': _devToolsUsage!.surveyShownCount,
                    },
                  },
                  machineMode: machineMode,
                );
                break;
              case apiIncrementSurveyShownCount:
                _devToolsUsage!.incrementSurveyShownCount();
                printOutput(
                  'DevTools Survey',
                  {
                    'id': id,
                    'result': {
                      'activeSurvey': _devToolsUsage!.activeSurvey,
                      'surveyShownCount': _devToolsUsage!.surveyShownCount,
                    },
                  },
                  machineMode: machineMode,
                );
                break;
              default:
                printOutput(
                  'Unknown DevTools Survey Request $surveyRequest',
                  {
                    'id': id,
                    'result': {
                      'activeSurvey': _devToolsUsage!.activeSurvey,
                      'surveyActionTaken': _devToolsUsage!.surveyActionTaken,
                      'surveyShownCount': _devToolsUsage!.surveyShownCount,
                    },
                  },
                  machineMode: machineMode,
                );
            }
            break;
          default:
            printOutput(
              'Unknown method ${json['method']}',
              {
                'id': id,
                'error': 'Unknown method ${json['method']}',
              },
              machineMode: machineMode,
            );
        }
      });
    }
  }

  return server;
}