| // Copyright (c) 2022, 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. | |
| // TODO(https://github.com/dart-lang/sdk/issues/48161): investigate whether or | |
| // not machine mode is still relevant. | |
| import 'dart:convert'; | |
| import 'dart:io'; | |
| import 'package:devtools_shared/devtools_server.dart'; | |
| import 'package:devtools_shared/devtools_shared.dart'; | |
| import 'package:vm_service/vm_service.dart'; | |
| import '../../devtools_server.dart'; | |
| import 'utils.dart'; | |
| import 'vs_code.dart'; | |
| class MachineModeCommandHandler { | |
| static const launchDevToolsService = 'launchDevTools'; | |
| static const copyAndCreateDevToolsFile = 'copyAndCreateDevToolsFile'; | |
| static const restoreDevToolsFile = 'restoreDevToolsFile'; | |
| static const errorLaunchingBrowserCode = 500; | |
| static const bool machineMode = true; | |
| MachineModeCommandHandler({required this.server}); | |
| /// The server instance associated with this client. | |
| final DevToolsServer server; | |
| // Only used for testing DevToolsUsage (used by survey). | |
| DevToolsUsage? _devToolsUsage; | |
| File? _devToolsBackup; | |
| static bool _isValidVmServiceUri(Uri? uri) { | |
| // Lots of things are considered valid URIs (including empty strings and | |
| // single letters) since they can be relative, so we need to do some extra | |
| // checks. | |
| return uri != null && | |
| uri.isAbsolute && | |
| (uri.isScheme('ws') || | |
| uri.isScheme('wss') || | |
| uri.isScheme('http') || | |
| uri.isScheme('https')); | |
| } | |
| Future<void> initialize({ | |
| required String devToolsUrl, | |
| required bool headlessMode, | |
| }) async { | |
| 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'] ?? <String, dynamic>{}; | |
| final method = json['method']; | |
| switch (method) { | |
| case 'vm.register': | |
| await _handleVmRegister(id, params, headlessMode, devToolsUrl); | |
| break; | |
| case 'devTools.launch': | |
| await _handleDevToolsLaunch(id, params, headlessMode, devToolsUrl); | |
| break; | |
| case 'client.list': | |
| _handleClientsList(id, params); | |
| break; | |
| case 'devTools.survey': | |
| _handleDevToolsSurvey(id, params); | |
| break; | |
| case 'vscode.extensions.discover': | |
| await _handleVsCodeExtensionsDiscover(id, params); | |
| break; | |
| default: | |
| DevToolsUtils.printOutput( | |
| 'Unknown method $method', | |
| { | |
| 'id': id, | |
| 'error': 'Unknown method $method', | |
| }, | |
| machineMode: machineMode, | |
| ); | |
| } | |
| }); | |
| } | |
| Future<void> _handleVmRegister( | |
| dynamic id, | |
| Map<String, dynamic> params, | |
| bool headlessMode, | |
| String devToolsUrl, | |
| ) async { | |
| if (!params.containsKey('uri')) { | |
| DevToolsUtils.printOutput( | |
| 'Invalid input: $params does not contain the key \'uri\'', | |
| { | |
| 'id': id, | |
| 'error': 'Invalid input: $params does not contain the key \'uri\'', | |
| }, | |
| machineMode: machineMode, | |
| ); | |
| } | |
| // params['uri'] should contain a vm service uri. | |
| final uri = Uri.tryParse(params['uri']); | |
| if (_isValidVmServiceUri(uri)) { | |
| await registerLaunchDevToolsService( | |
| uri!, | |
| id, | |
| devToolsUrl, | |
| headlessMode, | |
| ); | |
| } else { | |
| DevToolsUtils.printOutput( | |
| 'Uri must be absolute with a http, https, ws or wss scheme', | |
| { | |
| 'id': id, | |
| 'error': 'Uri must be absolute with a http, https, ws or wss scheme', | |
| }, | |
| machineMode: machineMode, | |
| ); | |
| } | |
| } | |
| Future<void> _handleDevToolsLaunch( | |
| dynamic id, | |
| Map<String, dynamic> params, | |
| bool headlessMode, | |
| String devToolsUrl, | |
| ) async { | |
| if (!params.containsKey('vmServiceUri')) { | |
| DevToolsUtils.printOutput( | |
| "Invalid input: $params does not contain the key 'vmServiceUri'", | |
| { | |
| 'id': id, | |
| 'error': | |
| "Invalid input: $params does not contain the key e'vmServiceUri'", | |
| }, | |
| machineMode: machineMode, | |
| ); | |
| } | |
| // params['vmServiceUri'] should contain a vm service uri. | |
| final vmServiceUri = Uri.tryParse(params['vmServiceUri'])!; | |
| if (_isValidVmServiceUri(vmServiceUri)) { | |
| try { | |
| final result = await server.launchDevTools( | |
| params, | |
| vmServiceUri, | |
| devToolsUrl, | |
| headlessMode, | |
| machineMode, | |
| ); | |
| DevToolsUtils.printOutput( | |
| 'DevTools launched', | |
| {'id': id, 'result': result}, | |
| machineMode: machineMode, | |
| ); | |
| } catch (e, s) { | |
| DevToolsUtils.printOutput( | |
| 'Failed to launch browser: $e\n$s', | |
| {'id': id, 'error': 'Failed to launch browser: $e\n$s'}, | |
| machineMode: machineMode, | |
| ); | |
| } | |
| } else { | |
| DevToolsUtils.printOutput( | |
| 'VM Service URI must be absolute with a http, https, ws or wss scheme', | |
| { | |
| 'id': id, | |
| 'error': | |
| 'VM Service Uri must be absolute with a http, https, ws or wss scheme', | |
| }, | |
| machineMode: machineMode, | |
| ); | |
| } | |
| } | |
| void _handleClientsList(dynamic id, Map<String, dynamic> params) { | |
| DevToolsUtils.printOutput( | |
| server.clientManager.toString(), | |
| server.clientManager.toJson(id), | |
| machineMode: machineMode, | |
| ); | |
| } | |
| Future<void> _handleVsCodeExtensionsDiscover( | |
| dynamic id, Map<String, dynamic> params) async { | |
| if (params case {'rootPaths': List rootPaths}) { | |
| final manager = VsCodeExtensionsManager(); | |
| DevToolsUtils.printOutput( | |
| 'Extensions', | |
| { | |
| 'id': id, | |
| 'result': { | |
| for (final rootPath in rootPaths.cast<String>()) | |
| rootPath: await manager.findVsCodeExtensions(rootPath), | |
| } | |
| }, | |
| machineMode: machineMode, | |
| ); | |
| } else { | |
| final errorMessage = | |
| "Invalid input: $params does not contain 'List<String> rootPaths'"; | |
| DevToolsUtils.printOutput( | |
| errorMessage, | |
| { | |
| 'id': id, | |
| 'error': errorMessage, | |
| }, | |
| machineMode: machineMode, | |
| ); | |
| } | |
| } | |
| void _handleDevToolsSurvey(dynamic id, Map<String, dynamic> params) { | |
| _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; | |
| DevToolsUtils.printOutput( | |
| 'DevTools Survey', | |
| { | |
| 'id': id, | |
| 'result': { | |
| 'success': true, | |
| }, | |
| }, | |
| machineMode: machineMode, | |
| ); | |
| } | |
| break; | |
| case restoreDevToolsFile: | |
| _devToolsUsage = null; | |
| final content = restoreDevToolsStore(); | |
| if (content != null) { | |
| DevToolsUtils.printOutput( | |
| 'DevTools Survey', | |
| { | |
| 'id': id, | |
| 'result': { | |
| 'success': true, | |
| 'content': content, | |
| }, | |
| }, | |
| machineMode: machineMode, | |
| ); | |
| _devToolsUsage = null; | |
| } | |
| break; | |
| case SurveyApi.setActiveSurvey: | |
| _devToolsUsage!.activeSurvey = value; | |
| DevToolsUtils.printOutput( | |
| 'DevTools Survey', | |
| { | |
| 'id': id, | |
| 'result': { | |
| 'success': _devToolsUsage!.activeSurvey == value, | |
| 'activeSurvey': _devToolsUsage!.activeSurvey, | |
| }, | |
| }, | |
| machineMode: machineMode, | |
| ); | |
| break; | |
| case SurveyApi.getSurveyActionTaken: | |
| DevToolsUtils.printOutput( | |
| 'DevTools Survey', | |
| { | |
| 'id': id, | |
| 'result': { | |
| 'activeSurvey': _devToolsUsage!.activeSurvey, | |
| 'surveyActionTaken': _devToolsUsage!.surveyActionTaken, | |
| }, | |
| }, | |
| machineMode: machineMode, | |
| ); | |
| break; | |
| case SurveyApi.setSurveyActionTaken: | |
| _devToolsUsage!.surveyActionTaken = jsonDecode(value); | |
| DevToolsUtils.printOutput( | |
| 'DevTools Survey', | |
| { | |
| 'id': id, | |
| 'result': { | |
| 'activeSurvey': _devToolsUsage!.activeSurvey, | |
| 'surveyActionTaken': _devToolsUsage!.surveyActionTaken, | |
| }, | |
| }, | |
| machineMode: machineMode, | |
| ); | |
| break; | |
| case SurveyApi.getSurveyShownCount: | |
| DevToolsUtils.printOutput( | |
| 'DevTools Survey', | |
| { | |
| 'id': id, | |
| 'result': { | |
| 'activeSurvey': _devToolsUsage!.activeSurvey, | |
| 'surveyShownCount': _devToolsUsage!.surveyShownCount, | |
| }, | |
| }, | |
| machineMode: machineMode, | |
| ); | |
| break; | |
| case SurveyApi.incrementSurveyShownCount: | |
| _devToolsUsage!.incrementSurveyShownCount(); | |
| DevToolsUtils.printOutput( | |
| 'DevTools Survey', | |
| { | |
| 'id': id, | |
| 'result': { | |
| 'activeSurvey': _devToolsUsage!.activeSurvey, | |
| 'surveyShownCount': _devToolsUsage!.surveyShownCount, | |
| }, | |
| }, | |
| machineMode: machineMode, | |
| ); | |
| break; | |
| default: | |
| DevToolsUtils.printOutput( | |
| 'Unknown DevTools Survey Request $surveyRequest', | |
| { | |
| 'id': id, | |
| 'result': { | |
| 'activeSurvey': _devToolsUsage!.activeSurvey, | |
| 'surveyActionTaken': _devToolsUsage!.surveyActionTaken, | |
| 'surveyShownCount': _devToolsUsage!.surveyShownCount, | |
| }, | |
| }, | |
| machineMode: machineMode, | |
| ); | |
| } | |
| } | |
| bool backupAndCreateDevToolsStore() { | |
| assert(_devToolsBackup == null); | |
| final devToolsStore = File( | |
| FileSystemExtension.devToolsStoreLocation, | |
| ); | |
| if (devToolsStore.existsSync()) { | |
| _devToolsBackup = devToolsStore | |
| .copySync('${FileSystemExtension.devToolsDir}/.devtools_backup_test'); | |
| devToolsStore.deleteSync(); | |
| } | |
| return true; | |
| } | |
| String? restoreDevToolsStore() { | |
| if (_devToolsBackup != null) { | |
| // Read the current ~/.devtools file | |
| fileSystem.maybeMoveLegacyDevToolsStore(); | |
| final devToolsStore = File(FileSystemExtension.devToolsStoreLocation); | |
| final content = devToolsStore.readAsStringSync(); | |
| // Delete the temporary ~/.devtools file | |
| devToolsStore.deleteSync(); | |
| if (_devToolsBackup!.existsSync()) { | |
| // Restore the backup ~/.devtools file we created in | |
| // backupAndCreateDevToolsStore. | |
| _devToolsBackup!.copySync(FileSystemExtension.devToolsStoreLocation); | |
| _devToolsBackup!.deleteSync(); | |
| _devToolsBackup = null; | |
| } | |
| return content; | |
| } | |
| return null; | |
| } | |
| Future<void> registerLaunchDevToolsService( | |
| Uri vmServiceUri, | |
| dynamic id, | |
| String devToolsUrl, | |
| bool headlessMode, | |
| ) async { | |
| try { | |
| // Connect to the vm service and register a method to launch DevTools in | |
| // chrome. | |
| final service = await DevToolsUtils.connectToVmService(vmServiceUri); | |
| if (service == null) return; | |
| service.registerServiceCallback( | |
| launchDevToolsService, | |
| (params) async { | |
| try { | |
| await server.launchDevTools( | |
| params, | |
| vmServiceUri, | |
| devToolsUrl, | |
| headlessMode, | |
| machineMode, | |
| ); | |
| return { | |
| 'result': Success().toJson(), | |
| }; | |
| } catch (e, s) { | |
| // Note: It's critical that we return responses in exactly the right format | |
| // or the VM will unregister the service. The objects must match JSON-RPC | |
| // however a successful response must also have a "type" field in its result. | |
| // Otherwise, we can return an error object (instead of result) that includes | |
| // code + message. | |
| return { | |
| 'error': { | |
| 'code': errorLaunchingBrowserCode, | |
| 'message': 'Failed to launch browser: $e\n$s', | |
| }, | |
| }; | |
| } | |
| }, | |
| ); | |
| final registerServiceMethodName = 'registerService'; | |
| await service.callMethod(registerServiceMethodName, args: { | |
| 'service': launchDevToolsService, | |
| 'alias': 'DevTools Server', | |
| }); | |
| DevToolsUtils.printOutput( | |
| 'Successfully registered launchDevTools service', | |
| { | |
| 'id': id, | |
| 'result': {'success': true}, | |
| }, | |
| machineMode: machineMode, | |
| ); | |
| } catch (e) { | |
| DevToolsUtils.printOutput( | |
| 'Unable to connect to VM service at $vmServiceUri: $e', | |
| { | |
| 'id': id, | |
| 'error': 'Unable to connect to VM service at $vmServiceUri: $e', | |
| }, | |
| machineMode: machineMode, | |
| ); | |
| } | |
| } | |
| } |