| // 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 _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', |
| ) |
| ..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 pub get for DDS in the local dart sdk'); |
| await processManager.runProcess( |
| CliCommand.dart(['pub', 'get']), |
| workingDirectory: path.join(localDartSdkLocation, 'pkg', 'dds'), |
| ); |
| |
| 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; |
| } |
| } |
| } |