2d15cad9e5
Change-Id: I05eb1af5dd985ee9f752b8ea409cabc785740472 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/283964 Commit-Queue: Devon Carew <devoncarew@google.com> Reviewed-by: Ben Konyi <bkonyi@google.com>
428 lines
12 KiB
Dart
428 lines
12 KiB
Dart
// 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';
|
|
|
|
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;
|
|
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,
|
|
);
|
|
}
|
|
|
|
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 apiSetActiveSurvey:
|
|
_devToolsUsage!.activeSurvey = value;
|
|
DevToolsUtils.printOutput(
|
|
'DevTools Survey',
|
|
{
|
|
'id': id,
|
|
'result': {
|
|
'success': _devToolsUsage!.activeSurvey == value,
|
|
'activeSurvey': _devToolsUsage!.activeSurvey,
|
|
},
|
|
},
|
|
machineMode: machineMode,
|
|
);
|
|
break;
|
|
case apiGetSurveyActionTaken:
|
|
DevToolsUtils.printOutput(
|
|
'DevTools Survey',
|
|
{
|
|
'id': id,
|
|
'result': {
|
|
'activeSurvey': _devToolsUsage!.activeSurvey,
|
|
'surveyActionTaken': _devToolsUsage!.surveyActionTaken,
|
|
},
|
|
},
|
|
machineMode: machineMode,
|
|
);
|
|
break;
|
|
case apiSetSurveyActionTaken:
|
|
_devToolsUsage!.surveyActionTaken = jsonDecode(value);
|
|
DevToolsUtils.printOutput(
|
|
'DevTools Survey',
|
|
{
|
|
'id': id,
|
|
'result': {
|
|
'activeSurvey': _devToolsUsage!.activeSurvey,
|
|
'surveyActionTaken': _devToolsUsage!.surveyActionTaken,
|
|
},
|
|
},
|
|
machineMode: machineMode,
|
|
);
|
|
break;
|
|
case apiGetSurveyShownCount:
|
|
DevToolsUtils.printOutput(
|
|
'DevTools Survey',
|
|
{
|
|
'id': id,
|
|
'result': {
|
|
'activeSurvey': _devToolsUsage!.activeSurvey,
|
|
'surveyShownCount': _devToolsUsage!.surveyShownCount,
|
|
},
|
|
},
|
|
machineMode: machineMode,
|
|
);
|
|
break;
|
|
case apiIncrementSurveyShownCount:
|
|
_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(
|
|
LocalFileSystem.devToolsStoreLocation(),
|
|
);
|
|
if (devToolsStore.existsSync()) {
|
|
_devToolsBackup = devToolsStore
|
|
.copySync('${LocalFileSystem.devToolsDir()}/.devtools_backup_test');
|
|
devToolsStore.deleteSync();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
String? restoreDevToolsStore() {
|
|
if (_devToolsBackup != null) {
|
|
// Read the current ~/.devtools file
|
|
LocalFileSystem.maybeMoveLegacyDevToolsStore();
|
|
|
|
final devToolsStore = File(LocalFileSystem.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(LocalFileSystem.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,
|
|
);
|
|
}
|
|
}
|
|
}
|