blob: a5c50ce85c32de0046b418b597b705de42eeddcd [file] [log] [blame]
// Copyright (c) 2017, 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.
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
import 'package:analysis_server/src/session_logger/process_id.dart';
import 'package:analysis_server/src/session_logger/session_logger.dart';
import 'package:analyzer/instrumentation/instrumentation.dart';
import 'package:analyzer_plugin/channel/channel.dart';
import 'package:analyzer_plugin/protocol/protocol.dart';
import 'package:analyzer_plugin/protocol/protocol_generated.dart';
/// The type of the function used to run a built-in plugin in an isolate.
typedef EntryPoint = void Function(SendPort sendPort);
/// A communication channel that allows an analysis server to send [Request]s
/// to, and to receive both [Response]s and [Notification]s from, a plugin.
final class ServerIsolateChannel implements ServerCommunicationChannel {
/// The instrumentation service that is being used by the analysis server.
final InstrumentationService instrumentationService;
/// The URI for the Dart file that will be run in the isolate that this
/// channel communicates with.
final Uri _pluginUri;
/// The URI for the package config file that controls how 'package:' URIs
/// are resolved.
final Uri _packageConfigUri;
/// The session logger that is to be used by this channel.
final SessionLogger _sessionLogger;
/// The isolate in which the plugin is running, or `null` if the plugin has
/// not yet been started by invoking [listen].
Isolate? _isolate;
/// The port used to send requests to the plugin, or `null` if the plugin has
/// not yet been started by invoking [listen].
SendPort? _sendPort;
/// The port used to receive responses and notifications from the plugin.
ReceivePort? _receivePort;
/// The port used to receive unhandled exceptions thrown in the plugin.
ReceivePort? _errorPort;
/// The port used to receive notification when the plugin isolate has exited.
ReceivePort? _exitPort;
ServerIsolateChannel(
this._pluginUri,
this._packageConfigUri,
this.instrumentationService,
this._sessionLogger,
);
/// The ID of the plugin running in the isolate, used to identify the plugin
/// to the instrumentation service.
String get _pluginId => _pluginUri.toString();
@override
void close() {
_receivePort?.close();
_errorPort?.close();
_exitPort?.close();
_isolate = null;
}
@override
void kill() {
_isolate?.kill(priority: Isolate.immediate);
}
@override
Future<void> listen(
void Function(Response response) onResponse,
void Function(Notification notification) onNotification, {
void Function(dynamic error)? onError,
void Function()? onDone,
}) async {
if (_isolate != null) {
throw StateError('Cannot listen to the same channel more than once.');
}
var receivePort = ReceivePort();
_receivePort = receivePort;
if (onError != null) {
var errorPort = ReceivePort();
_errorPort = errorPort;
errorPort.listen((error) {
onError(error);
});
}
if (onDone != null) {
var exitPort = ReceivePort();
_exitPort = exitPort;
exitPort.listen((_) {
onDone();
});
}
try {
_isolate = await _spawnIsolate();
} catch (exception, stackTrace) {
instrumentationService.logPluginError(
PluginData(_pluginId, null, null),
RequestErrorCode.PLUGIN_ERROR.toString(),
exception.toString(),
stackTrace.toString(),
);
if (onError != null) {
onError([exception.toString(), stackTrace.toString()]);
}
if (onDone != null) {
onDone();
}
close();
return;
}
var channelReady = Completer<void>();
receivePort.listen((dynamic input) {
if (input is SendPort) {
_sendPort = input;
channelReady.complete(null);
} else if (input is Map<String, Object?>) {
if (input.containsKey('id')) {
var encodedInput = json.encode(input);
instrumentationService.logPluginResponse(_pluginId, encodedInput);
_sessionLogger.logMessage(
from: ProcessId.plugin,
to: ProcessId.server,
message: input,
);
onResponse(Response.fromJson(input));
} else if (input.containsKey('event')) {
var encodedInput = json.encode(input);
instrumentationService.logPluginNotification(_pluginId, encodedInput);
_sessionLogger.logMessage(
from: ProcessId.plugin,
to: ProcessId.server,
message: input,
);
onNotification(Notification.fromJson(input));
}
}
});
return channelReady.future;
}
@override
void sendRequest(Request request) {
var sendPort = _sendPort;
if (sendPort != null) {
var jsonData = request.toJson();
var encodedRequest = json.encode(jsonData);
instrumentationService.logPluginRequest(_pluginId, encodedRequest);
_sessionLogger.logMessage(
from: ProcessId.server,
to: ProcessId.plugin,
message: jsonData,
);
sendPort.send(jsonData);
}
}
/// Spawns the isolate in which the plugin is running.
Future<Isolate> _spawnIsolate() {
return Isolate.spawnUri(
_pluginUri,
[],
_receivePort?.sendPort,
onError: _errorPort?.sendPort,
onExit: _exitPort?.sendPort,
packageConfig: _packageConfigUri,
);
}
}