| // Copyright 2022 The Flutter Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. | |
| import 'dart:async'; | |
| import 'dart:io'; | |
| import 'package:args/command_runner.dart'; | |
| import 'package:devtools_tool/model.dart'; | |
| import 'package:devtools_tool/utils.dart'; | |
| import 'package:io/io.dart'; | |
| import 'package:path/path.dart' as path; | |
| import 'shared.dart'; | |
| const _buildAppFlag = 'build-app'; | |
| // TODO(https://github.com/flutter/devtools/issues/7232): Consider using | |
| // AllowAnythingParser instead of manually passing these args through. | |
| const _machineFlag = 'machine'; | |
| const _dtdUriFlag = 'dtd-uri'; | |
| const _dtdExposedUriFlag = 'dtd-exposed-uri'; | |
| const _allowEmbeddingFlag = 'allow-embedding'; | |
| /// This command builds DevTools in release mode by running the | |
| /// `dt build` command and then serves DevTools with a locally | |
| /// running DevTools server. | |
| /// | |
| /// If the [_buildAppFlag] argument is negated (e.g. --no-build-app), then the | |
| /// DevTools web app will not be rebuilt before serving. The following arguments | |
| /// are ignored if '--no-build-app' is present in the list of arguments passed | |
| /// to this command. All of the following commands are passed along to the | |
| /// `dt build` command. | |
| /// | |
| /// If the [SharedCommandArgs.runApp] argument is passed (e.g. --run-app), then | |
| /// DevTools will be run with `flutter run` instead of being built with | |
| /// `flutter build web`. The DevTools web app running from Flutter Tool will be | |
| /// connected to a locally running instance of the DevTools server. | |
| /// | |
| /// If the [SharedCommandArgs.debugServer] argument is present, the DevTools | |
| /// server will be started with the `--observe` flag. This will allow you to | |
| /// debug and profile the server with a local VM service connection. By default, | |
| /// this will set `--pause-isolates-on-start` and | |
| /// `--pause-isolates-on-unhandled-exceptions` for the DevTools server VM | |
| /// service connection. | |
| /// | |
| /// If the [SharedCommandArgs.useFlutterFromPath] argument is present, the | |
| /// Flutter SDK will not be updated to the latest Flutter candidate before | |
| /// building DevTools. Use this flag to save the cost of updating the Flutter | |
| /// SDK when you already have the proper SDK checked out. This is helpful when | |
| /// developing with the DevTools server. | |
| /// | |
| /// If the [SharedCommandArgs.updatePerfetto] argument is present, the | |
| /// precompiled bits for Perfetto will be updated from the | |
| /// `dt update-perfetto` command as part of the DevTools build | |
| /// process. | |
| /// | |
| /// If [SharedCommandArgs.pubGet] argument is negated (e.g. --no-pub-get), then | |
| /// `dt pub-get --only-main` command will not be run before building | |
| /// the DevTools web app. Use this flag to save the cost of updating pub | |
| /// packages if your pub cahce does not need to be updated. This is helpful when | |
| /// developing with the DevTools server. | |
| /// | |
| /// The [SharedCommandArgs.buildMode] argument specifies the Flutter build mode | |
| /// that the DevTools web app will be built in ('release', 'profile', 'debug'). | |
| /// This defaults to 'release' if unspecified. | |
| class ServeCommand extends Command { | |
| ServeCommand() { | |
| argParser | |
| ..addFlag( | |
| _buildAppFlag, | |
| negatable: true, | |
| defaultsTo: true, | |
| help: | |
| 'Whether to build the DevTools web app before starting the DevTools' | |
| ' server. If --no-build-app is passed, the existing assets from' | |
| ' devtools_app/build/web will be used.', | |
| ) | |
| ..addFlag( | |
| SharedCommandArgs.runApp.flagName, | |
| negatable: false, | |
| defaultsTo: false, | |
| help: | |
| 'Whether to run the DevTools web app using `flutter run` instead' | |
| ' of building it using `flutter build web` and serving the assets' | |
| ' directly from the DevTools server.', | |
| ) | |
| ..addDebugServerFlag() | |
| ..addServeWithSdkOption() | |
| ..addUpdateFlutterFlag() | |
| ..addUpdatePerfettoFlag() | |
| ..addPubGetFlag() | |
| ..addBulidModeOption() | |
| ..addWasmFlag() | |
| ..addNoStripWasmFlag() | |
| ..addNoMinifyWasmFlag() | |
| // Flags defined in the server in DDS. | |
| ..addFlag( | |
| _machineFlag, | |
| negatable: false, | |
| help: 'Sets output format to JSON for consumption in tools.', | |
| ) | |
| ..addOption( | |
| _dtdUriFlag, | |
| help: 'Sets the dtd uri when starting the devtools server', | |
| ) | |
| ..addOption( | |
| _dtdExposedUriFlag, | |
| help: | |
| 'Sets the dtd uri that is exposed to the client when starting the devtools server', | |
| ) | |
| ..addFlag( | |
| _allowEmbeddingFlag, | |
| help: 'Allow embedding DevTools inside an iframe.', | |
| ); | |
| } | |
| static const _devToolsServerAddressLine = 'Serving DevTools at '; | |
| static const _debugServerVmServiceLine = | |
| 'The Dart VM service is listening on '; | |
| static const _debugServerDartDevToolsLine = | |
| 'The Dart DevTools debugger and profiler is available at: '; | |
| static const _runAppVmServiceLine = | |
| 'A Dart VM Service on Chrome is available at: '; | |
| static const _runAppFlutterDevToolsLine = | |
| 'The Flutter DevTools debugger and profiler on Chrome is available at: '; | |
| @override | |
| String get name => 'serve'; | |
| @override | |
| String get description => | |
| 'Builds DevTools in release mode and serves the web app with a locally ' | |
| 'running DevTools server.'; | |
| @override | |
| Future run() async { | |
| logStatus( | |
| 'WARNING: if you have local changes in packages/devtools_shared, you will' | |
| ' need to add a path dependency override in sdk/pkg/dds/pubspec.yaml in' | |
| ' order for these changes to be picked up.', | |
| ); | |
| final repo = DevToolsRepo.getInstance(); | |
| final processManager = ProcessManager(); | |
| final results = argResults!; | |
| final buildApp = results[_buildAppFlag] as bool; | |
| final runApp = results[SharedCommandArgs.runApp.flagName] as bool; | |
| final debugServer = results[SharedCommandArgs.debugServer.flagName] as bool; | |
| final updateFlutter = | |
| results[SharedCommandArgs.updateFlutter.flagName] as bool; | |
| final updatePerfetto = | |
| results[SharedCommandArgs.updatePerfetto.flagName] as bool; | |
| final useWasm = results[SharedCommandArgs.wasm.flagName] as bool; | |
| final noStripWasm = results[SharedCommandArgs.noStripWasm.flagName] as bool; | |
| final noMinifyWasm = | |
| results[SharedCommandArgs.noMinifyWasm.flagName] as bool; | |
| final runPubGet = results[SharedCommandArgs.pubGet.flagName] as bool; | |
| final devToolsAppBuildMode = | |
| results[SharedCommandArgs.buildMode.flagName] as String; | |
| final serveWithDartSdk = | |
| results[SharedCommandArgs.serveWithDartSdk.flagName] as String?; | |
| final forMachine = results[_machineFlag] as bool; | |
| // TODO(https://github.com/flutter/devtools/issues/8643): Support running in | |
| // machine mode with a debuggable DevTools app. | |
| if (runApp && forMachine) { | |
| throw Exception( | |
| 'Machine mode is not supported with `flutter run` DevTools.\n' | |
| 'Please use either --machine or --run-app, not both.\n' | |
| 'See https://github.com/flutter/devtools/issues/8643 for details.', | |
| ); | |
| } | |
| // Any flag that we aren't removing here is intended to be passed through. | |
| final remainingArguments = List.of(results.arguments) | |
| ..remove(SharedCommandArgs.updateFlutter.asArg()) | |
| ..remove(SharedCommandArgs.updateFlutter.asArg(negated: true)) | |
| ..remove(SharedCommandArgs.updatePerfetto.asArg()) | |
| ..remove(SharedCommandArgs.wasm.asArg()) | |
| ..remove(SharedCommandArgs.noStripWasm.asArg()) | |
| ..remove(SharedCommandArgs.noMinifyWasm.asArg()) | |
| ..remove(valueAsArg(_buildAppFlag)) | |
| ..remove(valueAsArg(_buildAppFlag, negated: true)) | |
| ..remove(SharedCommandArgs.runApp.asArg()) | |
| ..remove(SharedCommandArgs.debugServer.asArg()) | |
| ..remove(SharedCommandArgs.pubGet.asArg()) | |
| ..remove(SharedCommandArgs.pubGet.asArg(negated: true)) | |
| ..removeWhere( | |
| (element) => element.startsWith(SharedCommandArgs.buildMode.asArg()), | |
| ) | |
| ..removeWhere( | |
| (element) => element.startsWith( | |
| valueAsArg(SharedCommandArgs.serveWithDartSdk.flagName), | |
| ), | |
| ); | |
| final localDartSdkLocation = Platform.environment['LOCAL_DART_SDK']; | |
| if (localDartSdkLocation == null) { | |
| throw Exception( | |
| 'LOCAL_DART_SDK environment variable not set. Please add ' | |
| 'the following to your \'.bash_profile\' or \'.bashrc\' file:\n' | |
| 'export LOCAL_DART_SDK=<absolute/path/to/my/sdk>', | |
| ); | |
| } | |
| // Validate the path looks correct in case it was set without the /sdk or | |
| // similar. | |
| final pkgDir = Directory(path.join(localDartSdkLocation, 'pkg')); | |
| if (!pkgDir.existsSync()) { | |
| throw Exception( | |
| 'No pkg directory found in LOCAL_DART_SDK at "${pkgDir.path}"\n' | |
| 'Is LOCAL_DART_SDK set correctly to the "sdk" directory?', | |
| ); | |
| } | |
| final devToolsBuildLocation = path.join( | |
| repo.devtoolsAppDirectoryPath, | |
| 'build', | |
| 'web', | |
| ); | |
| if (buildApp && !runApp) { | |
| final process = await processManager.runProcess( | |
| CliCommand.tool([ | |
| 'build', | |
| SharedCommandArgs.updateFlutter.asArg(negated: !updateFlutter), | |
| if (updatePerfetto) SharedCommandArgs.updatePerfetto.asArg(), | |
| if (useWasm) SharedCommandArgs.wasm.asArg(), | |
| if (noStripWasm) SharedCommandArgs.noStripWasm.asArg(), | |
| if (noMinifyWasm) SharedCommandArgs.noMinifyWasm.asArg(), | |
| '${SharedCommandArgs.buildMode.asArg()}=$devToolsAppBuildMode', | |
| SharedCommandArgs.pubGet.asArg(negated: !runPubGet), | |
| ]), | |
| ); | |
| if (process.exitCode == 1) { | |
| throw Exception('Something went wrong while running `dt build`'); | |
| } | |
| logStatus('completed building DevTools: $devToolsBuildLocation'); | |
| } | |
| logStatus('running gclient sync in the local dart sdk'); | |
| await processManager.runProcess( | |
| CliCommand.gclient(['sync']), | |
| workingDirectory: localDartSdkLocation, | |
| ); | |
| logStatus('serving DevTools with a local devtools server...'); | |
| final ddsServeLocalScriptPath = path.join( | |
| 'pkg', | |
| 'dds', | |
| 'tool', | |
| 'devtools_server', | |
| 'serve_local.dart', | |
| ); | |
| // The address of the locally running DevTools server. | |
| String? devToolsServerAddress; | |
| // This is the DevTools URI associated with the DevTools server process | |
| // when the '--debug-server' flag is present. This DevTools connection | |
| // allows you to debug the DevTools server logic. | |
| String? debugServerDevToolsConnection; | |
| // This is the VM Service URI associated with the DevTools server process | |
| // when the '--debug-server' flag is present. | |
| String? debugServerVmServiceUri; | |
| void processServeLocalOutput(String line) { | |
| if (line.startsWith(_debugServerVmServiceLine)) { | |
| debugServerVmServiceUri = line | |
| .substring(_debugServerVmServiceLine.length) | |
| .trim(); | |
| } else if (line.startsWith(_debugServerDartDevToolsLine)) { | |
| debugServerDevToolsConnection = line | |
| .substring(_debugServerDartDevToolsLine.length) | |
| .trim(); | |
| } else if (line.startsWith(_devToolsServerAddressLine)) { | |
| // This will pull the server address from a String like: | |
| // "Serving DevTools at http://127.0.0.1:9104.". | |
| final regexp = RegExp( | |
| r'http:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+', | |
| ); | |
| final match = regexp.firstMatch(line); | |
| if (match != null) { | |
| devToolsServerAddress = match.group(0); | |
| } | |
| } | |
| } | |
| // This call will not exit until explicitly terminated by the user. | |
| final cliCommand = CliCommand.dart([ | |
| if (debugServer) ...['run', '--observe=0'], | |
| ddsServeLocalScriptPath, | |
| if (runApp) | |
| // When running DevTools via `flutter run`, the [flutterRunProcess] | |
| // below will launch DevTools in the browser. | |
| '--no-launch-browser' | |
| else | |
| // Only pass a build location if the server is serving the web assets | |
| // (i.e. not when DevTools app is ran via `flutter run`). | |
| '--devtools-build=$devToolsBuildLocation', | |
| // Pass any args that were provided to our script along. This allows IDEs | |
| // to pass `--machine` (etc.) so that this script can behave the same as | |
| // the "dart devtools" command for testing local DevTools/server changes. | |
| ...remainingArguments, | |
| ], sdkOverride: serveWithDartSdk); | |
| if (forMachine) { | |
| // If --machine flag is true, then the output is a tool-readable JSON. | |
| // Therefore, skip reading the process output and instead just run the | |
| // process. | |
| return processManager.runProcess( | |
| cliCommand, | |
| workingDirectory: localDartSdkLocation, | |
| ); | |
| } | |
| final serveLocalProcess = await startIndependentProcess( | |
| cliCommand, | |
| workingDirectory: localDartSdkLocation, | |
| waitForOutput: _devToolsServerAddressLine, | |
| onOutput: processServeLocalOutput, | |
| ); | |
| Process? flutterRunProcess; | |
| if (runApp) { | |
| if (devToolsServerAddress == null) { | |
| await _killProcess(serveLocalProcess); | |
| throw Exception( | |
| 'Cannot run DevTools and connect to the DevTools server because ' | |
| 'devToolsServerAddress is null.', | |
| ); | |
| } | |
| // This is the DevTools URI associated with the DevTools web app when it is | |
| // run using `flutter run` (e.g. when [runApp] is true). | |
| String? devToolsWebAppDevToolsConnection; | |
| // This is the VM service URI associated with the DevTools web app when it | |
| // is run using `flutter run` (e.g. when [runApp] is true). | |
| String? devToolsWebAppVmServiceUri; | |
| void processFlutterRunOutput(String line) { | |
| if (line.contains(_runAppVmServiceLine)) { | |
| final index = line.indexOf(_runAppVmServiceLine); | |
| devToolsWebAppVmServiceUri = line | |
| .substring(index + _runAppVmServiceLine.length) | |
| .trim(); | |
| } else if (line.contains(_runAppFlutterDevToolsLine)) { | |
| final index = line.indexOf(_runAppFlutterDevToolsLine); | |
| devToolsWebAppDevToolsConnection = line | |
| .substring(index + _runAppFlutterDevToolsLine.length) | |
| .trim(); | |
| } | |
| } | |
| logStatus('running DevTools'); | |
| flutterRunProcess = await startIndependentProcess( | |
| CliCommand.flutter([ | |
| 'run', | |
| '-d', | |
| 'chrome', | |
| // TODO(https://github.com/flutter/flutter/issues/160130): | |
| // [flutterRunProcess] exits without the --verbose flag. | |
| '--verbose', | |
| // Add the trailing slash because this is what DevTools app expects. | |
| '--dart-define=debug_devtools_server=$devToolsServerAddress/', | |
| ]), | |
| workingDirectory: repo.devtoolsAppDirectoryPath, | |
| waitForOutput: _runAppFlutterDevToolsLine, | |
| onOutput: processFlutterRunOutput, | |
| ); | |
| // Consolidate important stdout content for easy access. | |
| final debugServerContent = debugServer | |
| ? ''' | |
| - VM Service URI: $debugServerVmServiceUri | |
| - DevTools URI for debugging the DevTools server: $debugServerDevToolsConnection | |
| ''' | |
| : ''; | |
| print(''' | |
| ------------------------------------------------------------------- | |
| The DevTools web app should have just launched on Chrome. | |
| - VM Service URI: $devToolsWebAppVmServiceUri | |
| - DevTools URI for debugging the DevTools web app: $devToolsWebAppDevToolsConnection | |
| The DevTools server is running at: $devToolsServerAddress. | |
| $debugServerContent | |
| ------------------------------------------------------------------- | |
| '''); | |
| } | |
| await _waitForAndHandleExit( | |
| serveLocalProcess: serveLocalProcess, | |
| flutterRunProcess: flutterRunProcess, | |
| ); | |
| } | |
| Future<void> _waitForAndHandleExit({ | |
| required Process serveLocalProcess, | |
| required Process? flutterRunProcess, | |
| }) async { | |
| final serveLocalProcessExited = Completer<int>(); | |
| final flutterRunProcessExited = Completer<int>(); | |
| unawaited( | |
| serveLocalProcess.exitCode.then((code) { | |
| serveLocalProcessExited.complete(code); | |
| }), | |
| ); | |
| if (flutterRunProcess != null) { | |
| unawaited( | |
| flutterRunProcess.exitCode.then((code) { | |
| flutterRunProcessExited.complete(code); | |
| }), | |
| ); | |
| } | |
| await Future.any([ | |
| serveLocalProcessExited.future, | |
| flutterRunProcessExited.future, | |
| ]); | |
| if (serveLocalProcessExited.isCompleted && | |
| !flutterRunProcessExited.isCompleted && | |
| flutterRunProcess != null) { | |
| final exitCode = await serveLocalProcessExited.future; | |
| logStatus( | |
| 'Killing the flutterRunProcess because the serveLocalProcess has ' | |
| 'exited with code $exitCode.', | |
| ); | |
| await _killProcess(flutterRunProcess); | |
| } | |
| if (flutterRunProcessExited.isCompleted && | |
| !serveLocalProcessExited.isCompleted) { | |
| final exitCode = await flutterRunProcessExited.future; | |
| logStatus( | |
| 'Killing the serveLocalProcess because the flutterRunProcess has ' | |
| 'exited with code $exitCode.', | |
| ); | |
| await _killProcess(serveLocalProcess); | |
| } | |
| } | |
| Future<void> _killProcess(Process? process) async { | |
| if (process != null) { | |
| Process.killPid(process.pid, ProcessSignal.sigint); | |
| await process.exitCode; | |
| } | |
| } | |
| } |