blob: 6e93fc647cc0b660f8b32f03aefd48f807d0bf38 [file] [log] [blame]
// Copyright (c) 2019, 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.
// @dart = 2.9
import 'dart:async';
import 'dart:convert';
import 'package:logging/logging.dart';
import 'package:vm_service/vm_service.dart';
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
import 'events.dart';
import 'services/chrome_proxy_service.dart' show ChromeProxyService;
import 'services/debug_service.dart';
final _logger = Logger('DwdsVmClient');
// A client of the vm service that registers some custom extensions like
// hotRestart.
class DwdsVmClient {
final VmService client;
final StreamController<Map<String, Object>> _requestController;
final StreamController<Map<String, Object>> _responseController;
static const int kFeatureDisabled = 100;
static const String kFeatureDisabledMessage = 'Feature is disabled.';
/// Null until [close] is called.
///
/// All subsequent calls to [close] will return this future.
Future<void> _closed;
DwdsVmClient(this.client, this._requestController, this._responseController);
Future<void> close() => _closed ??= () async {
await _requestController.close();
await _responseController.close();
await client.dispose();
}();
static Future<DwdsVmClient> create(DebugService debugService) async {
// Set up hot restart as an extension.
var requestController = StreamController<Map<String, Object>>();
var responseController = StreamController<Map<String, Object>>();
VmServerConnection(requestController.stream, responseController.sink,
debugService.serviceExtensionRegistry, debugService.chromeProxyService);
var client =
VmService(responseController.stream.map(jsonEncode), (request) {
if (requestController.isClosed) {
_logger.warning(
'Attempted to send a request but the connection is closed:\n\n'
'$request');
return;
}
requestController.sink.add(jsonDecode(request) as Map<String, dynamic>);
});
var chromeProxyService =
debugService.chromeProxyService as ChromeProxyService;
// Register '_flutter.listViews' method on the chrome proxy service vm.
// In native world, this method is provided by the engine, but the web
// engine is not aware of the VM uri or the isolates.
//
// Issue: https://github.com/dart-lang/webdev/issues/1315
client.registerServiceCallback('_flutter.listViews', (request) async {
final vm = await chromeProxyService.getVM();
final isolates = vm.isolates;
return <String, dynamic>{
'result': <String, Object>{
'views': <Object>[
for (var isolate in isolates)
<String, Object>{
'id': isolate.id,
'isolate': isolate.toJson(),
}
],
}
};
});
await client.registerService('_flutter.listViews', 'DWDS listViews');
client.registerServiceCallback('hotRestart', (request) async {
_logger.info('Attempting a hot restart');
chromeProxyService.terminatingIsolates = true;
await _disableBreakpointsAndResume(client, chromeProxyService);
int context;
try {
_logger.info('Attempting to get execution context ID.');
context = await chromeProxyService.executionContext.id;
_logger.info('Got execution context ID.');
} on StateError catch (e) {
// We couldn't find the execution context. `hotRestart` may have been
// triggered in the middle of a full reload.
return {
'error': {
'code': RPCError.kInternalError,
'message': e.message,
}
};
}
// Start listening for isolate create events before issuing a hot
// restart. Only return success after the isolate has fully started.
var stream = chromeProxyService.onEvent('Isolate');
try {
_logger.info('Issuing \$dartHotRestart request.');
await chromeProxyService.remoteDebugger
.sendCommand('Runtime.evaluate', params: {
'expression': r'$dartHotRestart();',
'awaitPromise': true,
'contextId': context,
});
_logger.info('\$dartHotRestart request complete.');
} on WipError catch (exception) {
var code = exception.error['code'];
// This corresponds to `Execution context was destroyed` which can
// occur during a hot restart that must fall back to a full reload.
if (code != RPCError.kServerError) {
return {
'error': {
'code': exception.error['code'],
'message': exception.error['message'],
'data': exception,
}
};
}
}
_logger.info('Waiting for Isolate Start event.');
await stream.firstWhere((event) => event.kind == EventKind.kIsolateStart);
chromeProxyService.terminatingIsolates = false;
_logger.info('Successful hot restart');
return {'result': Success().toJson()};
});
await client.registerService('hotRestart', 'DWDS fullReload');
client.registerServiceCallback('fullReload', (_) async {
_logger.info('Attempting a full reload');
await chromeProxyService.remoteDebugger.enablePage();
await chromeProxyService.remoteDebugger.pageReload();
_logger.info('Successful full reload');
return {'result': Success().toJson()};
});
await client.registerService('fullReload', 'DWDS');
client.registerServiceCallback('ext.dwds.screenshot', (_) async {
await chromeProxyService.remoteDebugger.enablePage();
var response = await chromeProxyService.remoteDebugger
.sendCommand('Page.captureScreenshot');
return {'result': response.result};
});
await client.registerService('ext.dwds.screenshot', 'DWDS');
client.registerServiceCallback('ext.dwds.emitEvent', (event) async {
emitEvent(DwdsEvent(
event['type'] as String, event['payload'] as Map<String, dynamic>));
return {'result': Success().toJson()};
});
await client.registerService('ext.dwds.emitEvent', 'DWDS');
client.registerServiceCallback('_yieldControlToDDS', (request) async {
final ddsUri = request['uri'] as String;
if (ddsUri == null) {
return RPCError(
request['method'] as String,
RPCError.kInvalidParams,
"'Missing parameter: 'uri'",
).toMap();
}
return DebugService.yieldControlToDDS(ddsUri)
? {'result': Success().toJson()}
: {
'error': {
'code': kFeatureDisabled,
'message': kFeatureDisabledMessage,
'details':
'Existing VM service clients prevent DDS from taking control.',
},
};
});
await client.registerService('_yieldControlToDDS', 'DWDS');
return DwdsVmClient(client, requestController, responseController);
}
}
Future<void> _disableBreakpointsAndResume(
VmService client, ChromeProxyService chromeProxyService) async {
_logger.info('Attempting to disabling breakpoints and resume the isolate');
var vm = await client.getVM();
if (vm.isolates.isEmpty) throw StateError('No active isolate to resume.');
var isolateRef = vm.isolates.first;
var isolate = await client.getIsolate(isolateRef.id);
await chromeProxyService.disableBreakpoints();
if (isolate.pauseEvent.kind == EventKind.kPauseInterrupted ||
isolate.pauseEvent.kind == EventKind.kPauseBreakpoint) {
await client.resume(isolate.id);
}
_logger.info('Successfully disabled breakpoints and resumed the isolate');
}